Метод копирования CRTP предупреждает о потенциальной утечке памяти

У меня есть иерархия объектов, и мне нужно иметь возможность клонировать объекты из базового класса. Я следовал типичному шаблону CRTP, за исключением того, что я также хочу иметь возможность возвращать дочерний класс, если копия вызывается непосредственно для дочернего элемента. Для этого я последовал предложению здесь: https://stackoverflow.com/a/30252692/1180785

Кажется, это работает нормально, но Clang предупреждает меня о возможной утечке памяти. Я сократил код до этого MCVE:

template <typename T>
class CRTP {
protected:
    virtual CRTP<T> *internal_copy(void) const {
        return new T(static_cast<const T&>(*this));
    }

public:
    T *copy(void) const {
        return static_cast<T*>(internal_copy());
    }

    virtual ~CRTP(void) = default;
};

class Impl : public CRTP<Impl> {
};

int main(void) {
    Impl a;
    Impl *b = a.copy();
    delete b;
}

Насколько я могу судить, там нет возможной утечки памяти, но запуск Clang через XCode показывает следующее:

Clang потенциальная утечка памяти

Есть ли утечка памяти? Если нет, то что вызывает ложное срабатывание и как я могу это обойти? (Я бы предпочел не отключать статический анализ)


person Dave    schedule 11.12.2016    source источник
comment
Показанная вами программа вообще не вызывает CRTP::copy. Я подозреваю, что код, который вы запускаете, может отличаться от того, который вы показываете.   -  person Igor Tandetnik    schedule 11.12.2016
comment
@IgorTandetnik хорошее замечание; Я пропустил это, пока уменьшал. Однако предупреждение, которое я разместил, взято непосредственно из кода, который я разместил, поэтому каким-то образом он действительно запускает CRTP::copy. Это заставляет меня думать, что на самом деле это может быть ошибка в анализаторе, связанная с виртуальными методами.   -  person Dave    schedule 11.12.2016
comment
@IgorTandetnik Я обновил код с улучшенной демонстрацией проблемы, которая удаляет ненужную виртуальную копию на копии и удаляет массив. Это действительно вызывает метод CRTP::copy, и анализ clang такой же.   -  person Dave    schedule 11.12.2016
comment
Что бы это ни стоило, я не вижу утечки памяти. Не уверен, почему анализатор жалуется. Возможно, ему не нравится какая-либо функция, возвращающая необработанный указатель на выделенную память; или, может быть, танец шаблонов сбивает его с толку настолько, что он не может доказать, что вызывающий код действительно освобождает память. Рассмотрите возможность возврата std::unique_ptr<T> вместо необработанного указателя.   -  person Igor Tandetnik    schedule 11.12.2016
comment
@IgorTandetnik даже при использовании unique_ptr (std::unique_ptr<T> copy(void) const { return std::unique_ptr<T>(static_cast<T*>(internal_copy())); }) выдает предупреждение. Интересно, что я обнаружил, что он исчезает, если я изменю способ связи copy и internal_copy (т.е. если вся логика находится в копии, без вызова internal_copy, все в порядке)   -  person Dave    schedule 11.12.2016
comment
Почему internal_copy должен возвращать CRTP<T>*, а не только T*? Кроме того, поскольку T является основой CRTP<T>, зачем копировать его static_cast? Интересно, думает ли анализатор, что вы как-то теряете информацию со всеми изменениями типа указателя. Просто дикая догадка...   -  person cyberbisson    schedule 12.12.2016
comment
@cyberbisson смотрите связанный ответ, чтобы узнать, почему так должно быть. Это в основном потому, что он не может переопределить существующий метод, чтобы вернуть тип, который еще не знает, что он является подклассом оригинала (базовый класс был удален из моего сокращенного примера здесь). Также T не является основой CRTP<T>; CRTP<T> является основой T (именно это делает его удивительно рекурсивным!)   -  person Dave    schedule 12.12.2016
comment
@ Дэйв О да, конечно. Я просмотрел ссылку, но не полностью понял проблему ковариации, которая проявлялась только тогда, когда она была переопределена. Игнорировать игнорировать игнорировать. :)   -  person cyberbisson    schedule 12.12.2016


Ответы (2)


Я нашел обходной путь, который делает анализатор счастливым, но при этом позволяет использовать этот паттерн. Просто поменяйте местами связь между copy и internal_copy:

template <typename T>
class CRTP : public Base {
protected:
    virtual CRTP<T> *internal_copy(void) const {
        return copy();
    }

public:
    T *copy(void) const {
        return new T(static_cast<const T&>(*this));
    }
};

Это все еще работает в контексте исходного предложения здесь, потому что при разрешении copy внутри CRTP он предпочтет переопределение CRTP (даже хотя это не виртуальный метод), поэтому бесконечного цикла нет.

Что касается того, почему анализатор доволен этим порядком, но недоволен первоначальным порядком, я понятия не имею.

person Dave    schedule 11.12.2016

Я думаю, анализатор сбит с толку static_cast в вашем методе copy. Если вы просто измените его на dynamic_cast, он перестанет помечать строку как утечку памяти. Это заставляет меня поверить, что он думает, что вы, возможно, приводите экземпляр базового типа (CRTP<T>) к его производному типу (T), который, конечно, будет недействительным при разыменовании. Вы, очевидно, этого не делаете, так что это может быть ошибка в детекторе утечек, или это может быть что-то более тонкое, чем то, о чем я сейчас думаю. Сейчас это может не быть ошибкой, но если Impl станет более сложным типом (например, с множественным наследованием), это может стать проблемой (как и ваш static_cast в вызове copy-CTOR).

Эта реализация (с меньшим количеством приведений) также не имела утечек для меня:

template <typename T>
class CRTP {
protected:
    virtual T *internal_copy() const {
        return new T(static_cast<const T&>(*this));
    }

public:
    T *copy() const {
        return internal_copy();
    }

    virtual ~CRTP() = default;
};
person cyberbisson    schedule 12.12.2016
comment
См. мой ответ на ваш комментарий для объяснения, почему internal_copy не может вернуть T* в этом случае использования. Интересно, что dynamic_cast делает его счастливым; это не то, что я тестировал, потому что я использую -no-rtti, но, как вы говорите; возможно, он просто думает, что static_cast может привести к неправильному типу. - person Dave; 12.12.2016