Будет ли «пустой» конструктор или деструктор делать то же самое, что и сгенерированный? - PullRequest
71 голосов
/ 22 июня 2009

Предположим, у нас есть (игрушечный) класс C ++, такой как:

class Foo {
    public:
        Foo();
    private:
        int t;
};

Поскольку деструктор не определен, компилятор C ++ должен автоматически его создать для класса Foo. Если деструктору не нужно очищать какую-либо динамически распределенную память (то есть мы могли бы разумно полагаться на деструктор, предоставляемый нам компилятором), он определит пустой деструктор, т. Е.

Foo::~Foo() { }

сделать то же самое, что и сгенерированный компилятором? А как насчет пустого конструктора - то есть Foo::Foo() { }?

Если есть различия, где они существуют? Если нет, один метод предпочтительнее другого?

Ответы [ 7 ]

115 голосов
/ 22 июня 2009

Это будет делать то же самое (ничего, по сути). Но это не то же самое, что если бы ты не написал это. Потому что для написания деструктора потребуется рабочий деструктор базового класса. Если деструктор базового класса является закрытым или если по какой-либо другой причине он не может быть вызван, то ваша программа неисправна. Учтите это

struct A { private: ~A(); };
struct B : A { }; 

Это нормально, если вам не требуется уничтожать объект типа B (и, следовательно, неявно типа A) - например, если вы никогда не вызываете delete для динамически создаваемого объекта или никогда не создаете объект об этом в первую очередь. Если вы это сделаете, то компилятор отобразит соответствующую диагностику. Теперь, если вы предоставите один явно

struct A { private: ~A(); };
struct B : A { ~B() { /* ... */ } }; 

Тот попытается неявно вызвать деструктор базового класса и вызовет диагностику уже во время определения ~B.

Существует еще одно различие, которое сосредоточено вокруг определения деструктора и неявных вызовов членов-деструкторов. Рассмотрим этот умный указатель

struct C;
struct A {
    auto_ptr<C> a;
    A();
};

Предположим, что объект типа C создан в определении конструктора A в файле .cpp, который также содержит определение struct C. Теперь, если вы используете struct A и требуете уничтожения объекта A, компилятор предоставит неявное определение деструктора, как в случае выше. Этот деструктор также будет неявно вызывать деструктор объекта auto_ptr. И это приведет к удалению имеющегося у него указателя, указывающего на объект C - без знания определения C! Это появилось в файле .cpp, где определен конструктор структуры A.

На самом деле это распространенная проблема в реализации идиомы pimpl. Решением здесь является добавление деструктора и предоставление его пустого определения в файле .cpp, где определена структура C. В тот момент, когда он вызывает деструктор своего члена, он узнает определение struct C и может правильно вызвать его деструктор.

struct C;
struct A {
    auto_ptr<C> a;
    A();
    ~A(); // defined as ~A() { } in .cpp file, too
};

Обратите внимание, что boost::shared_ptr не имеет этой проблемы: вместо этого требуется полный тип, когда его конструктор вызывается определенным образом.

Еще один момент, в котором это имеет значение в текущем C ++, - это когда вы хотите использовать memset и друзей для такого объекта, у которого пользователь объявил деструктор. Такие типы больше не являются POD (обычные старые данные), и их нельзя копировать битами. Обратите внимание, что это ограничение на самом деле не нужно - и следующая версия C ++ улучшила ситуацию, позволив вам по-прежнему копировать биты таких типов до тех пор, пока не будут внесены другие более важные изменения.


Так как вы попросили конструкторов: ну, для этого почти все то же самое верно. Обратите внимание, что конструкторы также содержат неявные вызовы деструкторов. В таких вещах, как auto_ptr, эти вызовы (даже если они на самом деле не выполняются во время выполнения - чистая возможность здесь уже имеет значение) принесут тот же вред, что и для деструкторов, и произойдут, когда что-то в конструкторе вызовет - тогда компилятору потребуется вызвать деструктор членов. В этом ответе используется неявное определение конструкторов по умолчанию.

Кроме того, то же самое верно для видимости и PODness, которые я сказал о деструкторе выше.

Есть одно важное отличие в отношении инициализации. Если вы поместите конструктор, объявленный пользователем, ваш тип больше не получит инициализацию значений членов, и ваш конструктор должен выполнить любую необходимую инициализацию. Пример: * * тысяча сорок-две

struct A {
    int a;
};

struct B {
    int b;
    B() { }
};

В этом случае всегда верно следующее

assert(A().a == 0);

Хотя следующее поведение не определено, потому что b никогда не инициализировался (ваш конструктор пропустил это). Значение может быть нулевым, но может быть и любым другим странным значением. Попытка чтения из такого неинициализированного объекта приводит к неопределенному поведению.

assert(B().b == 0);

Это также верно для использования этого синтаксиса в new, например new A() (обратите внимание на круглые скобки в конце - если они опущены, инициализация значения не выполняется, и поскольку не существует объявленного пользователем конструктора, который мог бы его инициализировать , a останется неинициализированным).

17 голосов
/ 24 февраля 2010

Я знаю, что опаздываю в обсуждении, тем не менее мой опыт говорит о том, что компилятор ведет себя иначе, когда сталкивается с пустым деструктором по сравнению с сгенерированным компилятором. По крайней мере, так обстоит дело с MSVC ++ 8.0 (2005) и MSVC ++ 9.0 (2008).

Просматривая сгенерированную сборку для некоторого кода, использующего шаблоны выражений, я понял, что в режиме выпуска вызов моего BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) никогда не был встроенным. (пожалуйста, не обращайте внимания на точные типы и подпись оператора).

Для дальнейшей диагностики проблемы я включил различные предупреждения компилятора, которые по умолчанию отключены . Предупреждение C4714 особенно интересно. Он генерируется компилятором, когда функция, помеченная __forceinline , тем не менее, не становится встроенной .

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

Среди причин, описанных в документации, компилятору не удалось встроить функцию, помеченную __forceinline для:

Функции, возвращающие раскручиваемый объект по значению, когда -GX / EHs / EHa включен

Это случай моего BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs). BinaryVectorExpression возвращается по значению, и даже если его деструктор пуст, это возвращаемое значение рассматривается как объект, который нельзя развернуть. Добавление throw () к деструктору не помогло компилятору, а я все равно избегаю использования спецификаций исключений . Закомментирование пустого деструктора позволило компилятору полностью встроить код.

Вывод заключается в том, что теперь в каждом классе я пишу закомментированные пустые деструкторы, чтобы люди знали, что деструктор не делает ничего нарочно, точно так же, как люди комментируют пустую спецификацию исключений `/ * throw () * /, чтобы указать, что деструктор не может бросить.

//~Foo() /* throw() */ {}

Надеюсь, это поможет.

12 голосов
/ 22 июня 2009

Пустой деструктор, который вы определили вне класса, имеет сходную семантику в большинстве случаев, но не во всех.

В частности, неявно определенный деструктор
1) является встроенным публичным участником (ваш не встроенный)
2) обозначается как тривиальный деструктор (необходимо создавать тривиальные типы, которые могут быть в союзах, а ваши нет)
3) имеет спецификацию исключений (throw (), ваш нет)

8 голосов
/ 22 июня 2009

Да, этот пустой деструктор такой же, как и автоматически созданный. Я всегда просто позволяю компилятору генерировать их автоматически; Я не думаю, что необходимо явно указывать деструктор, если вам не нужно делать что-то необычное: скажем, виртуальное или личное.

3 голосов
/ 22 июня 2009

Я согласен с Дэвидом, за исключением того, что я бы сказал, что в общем случае хорошей практикой является определение виртуального деструктора, т.е.

virtual ~Foo() { }

пропуск виртуального деструктора может привести к утечке памяти, потому что люди, унаследованные от вашего класса Foo, могут не заметить, что их деструктор никогда не будет вызван !!

1 голос
/ 22 июня 2009

Лучше всего поставить пустую декларацию, она говорит всем будущим сопровождающим, что это не было оплошностью, и вы действительно хотели использовать стандартную.

0 голосов
/ 14 августа 2009

Пустое определение подходит, так как на определение можно ссылаться

virtual ~GameManager() { };
Пустое объявление обманчиво похоже на внешний вид
virtual ~GameManager();
, но предлагает страшное без определения виртуального деструктора ошибка
Undefined symbols:
  "vtable for GameManager", referenced from:
      __ZTV11GameManager$non_lazy_ptr in GameManager.o
      __ZTV11GameManager$non_lazy_ptr in Main.o
ld: symbol(s) not found
...