2009-06-22 3 views
65

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

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

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

Foo::~Foo() { } 

сделать то же самое, что и с помощью компилятора? Как насчет пустого конструктора, то есть Foo::Foo() { }?

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

+0

Я немного изменил этот вопрос, чтобы превратить редактирование afterthoguht в фактическую часть вопроса. Если есть какие-то синтаксические ошибки в тех частях, которые я редактировал, кричите на меня, а не на оригинального. @ Андрю, если вы чувствуете, что я слишком сильно изменил ваш вопрос, не стесняйтесь его возвращать; если вам нравится изменение, но думаю, что этого недостаточно, вы, очевидно, можете отредактировать свой собственный вопрос. –

ответ

112

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

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

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

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

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

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

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

Давайте предположим, что объект типа C создается в определении конструктор в файле .cpp, который также содержит определение структуры 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, эти вызовы (даже если они не выполняются во время выполнения - чистая возможность уже здесь имеет значение) будут иметь тот же самый вред, что и для деструкторов, и происходят, когда что-то в конструкторе бросает - тогда компилятор должен вызвать деструктор членов. This answer использует неявное определение конструкторов по умолчанию.

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

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

struct A { 
    int a; 
}; 

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

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

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

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

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

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

+0

+1 для упоминания форвардных объявленных автоматических указателей и автоматического деструктора. Обычная магия, когда вы начинаете декларировать вещи. –

+1

Ваш первый пример немного странный. B, который вы написали, не может быть использован вообще (новый - это ошибка, любое нажатие на одно будет неопределенным поведением, поскольку оно не является POD). –

+0

Кроме того, A(). A == 0 относится только к статике. Локальная переменная типа A будет неинициализирована. –

8

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

11

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

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

+1

Примечание к 3: спецификация исключения не всегда пуста в неявно определенном деструкторе, как указано в [except.spec]. – dalle

+0

@dalle +1 на комментарий - спасибо за внимание к этому - вы действительно правы, если Foo получил из базовых классов каждый с неявными деструкторами с спецификациями исключений - неявный dtor Foo бы «унаследовал» объединение этого исключения спецификации - в этом случае, поскольку наследование отсутствует, спецификация исключения неявного dtor оказывается throw(). –

1

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

2

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

virtual ~Foo() { } 

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

0

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

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

16

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

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

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

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

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

функции, возвращающей unwindable объекта по значению, когда -GX/EHS/EHa на

Это касается моего BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs). BinaryVectorExpression возвращается по значению, и хотя его деструктор пуст, он делает это возвращаемое значение рассматриваемым как незаменимый объект. Добавление throw() к деструктору не помогло компилятору и I avoid using exception specifications anyway. Комментируя пустой деструктор, компилятор полностью встраивает код.

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

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

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