Будет ли "пустой" конструктор или деструктор делать то же самое, что и сгенерированный?

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

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

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

Foo::~Foo() { }

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

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

Ответ 1

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

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, где определен конструктор struct 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 не имеет этой проблемы: вместо этого требуется полный тип, когда его конструктор вызывается определенными способами.

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


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

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

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

struct A {
    int a;
};

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

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

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

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

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

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

Ответ 2

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

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

Чтобы еще больше диагностировать проблему, я включил различные Предупреждения компилятора, которые отключены по умолчанию. Предупреждение В любом случае я избегаю использования спецификаций исключений. Комментируя пустой деструктор, компилятор полностью встраивает код.

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

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

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

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

virtual ~Foo() { }

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

Ответ 6

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

Ответ 7

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

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