2009-06-22 7 views
65

Angenommen, wir haben ein (Spielzeug) C++ Klasse wie die folgenden:Wird ein 'leerer' Konstruktor oder Destruktor dasselbe tun wie das generierte?

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

Da kein destructor definiert wird, sollte ein C++ Compiler eine automatisch für die Klasse Foo erstellen. Wenn der Destruktor keinen dynamisch zugewiesenen Speicher bereinigen muss (das heißt, wir können uns vernünftigerweise auf den Destruktor verlassen, den der Compiler uns gibt), wird ein leerer Destruktor definiert, d.

Foo::~Foo() { } 

dasselbe tun, wie der vom Compiler erzeugte? Was ist mit einem leeren Konstruktor - also Foo::Foo() { }?

Wenn es Unterschiede gibt, wo existieren sie? Wenn nicht, ist eine Methode gegenüber der anderen bevorzugt?

+0

Ich habe diese Frage ein wenig modifiziert, um die Nachbearbeitung zu einem eigentlichen Teil der Frage zu machen. Wenn es Syntaxfehler in den Teilen gibt, die ich bearbeitet habe, schreie mich an, nicht den ursprünglichen Fragesteller. @Andrew, wenn du das Gefühl hast, dass ich deine Frage zu sehr geändert habe, kannst du sie gerne rückgängig machen. Wenn du die Änderung magst, aber denkst, dass es nicht genug ist, kannst du natürlich deine eigene Frage bearbeiten. –

Antwort

112

Es wird das gleiche tun (nichts, im Wesentlichen). Aber es ist nicht dasselbe, als wenn du es nicht geschrieben hättest. Da das Schreiben des Destruktors einen funktionierenden Basisklassen-Destruktor erfordert. Wenn der Basisklassendestruktor privat ist oder aus einem anderen Grund nicht aufgerufen werden kann, ist Ihr Programm fehlerhaft. Betrachten Sie diese

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

Das OK ist, solange Ihr nicht erfordern ein Objekt vom Typ B zu zerstören (und damit implizit vom Typ A) - wie wenn Sie noch nie auf einem dynamisch erstellte Objekt löschen aufrufen, oder du erstellst niemals ein Objekt von ihm. Wenn Sie dies tun, zeigt der Compiler eine entsprechende Diagnose an. Nun, wenn Sie eine sehen ausdrücklich

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

Dass man wird versuchen, implizit den Destruktor der Basisklasse aufrufen, und wird eine Diagnose bereits zur Definitionszeit von ~B verursachen.

Es gibt einen weiteren Unterschied, der sich um die Definition des Destruktors und implizite Aufrufe von Elementdestruktoren dreht.Betrachten Sie dieses Smart-Pointer-Mitglied

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

Lassen Sie sich das Objekt vom Typ annimmt C wird in der Definition von A Konstruktor in der .cpp-Datei erstellt, die auch die Definition der Struktur enthalten C. Wenn Sie jetzt die Struktur A verwenden und die Zerstörung eines Objekts A erfordern, stellt der Compiler wie im obigen Fall eine implizite Definition des Destruktors bereit. Dieser Destruktor ruft auch implizit den Destruktor des Objekts auto_ptr auf. Und das wird den Zeiger löschen, der es enthält, der auf das Objekt C zeigt - ohne die Definition von C zu kennen! Das erschien in der Datei .cpp, wo der Konstruktor von struct A definiert ist.

Dies ist tatsächlich ein häufiges Problem bei der Implementierung der Pimpl-Idiom. Die Lösung besteht darin, einen Destruktor hinzuzufügen und eine leere Definition davon in der Datei anzugeben, in der die Struktur C definiert ist. Zu dem Zeitpunkt, zu dem es den Destruktor seines Members aufruft, wird es die Definition der Struktur C kennen und kann seinen Destruktor korrekt aufrufen.

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

Beachten Sie, dass boost::shared_ptr dieses Problem nicht hat: Es erfordert stattdessen eine komplette Art, wenn sein Konstruktor in gewisser Weise aufgerufen wird.

Ein weiterer Punkt, wo es einen Unterschied in aktuellen C++ macht, ist, wenn Sie memset und Freunde auf ein solches Objekt verwenden möchten, das einen vom Benutzer deklarierten Destruktor hat. Solche Typen sind keine PODs mehr (einfache alte Daten), und diese dürfen nicht bitkopiert werden. Beachten Sie, dass diese Einschränkung nicht wirklich benötigt wird - und die nächste C++ - Version hat die Situation in diesem Fall verbessert, so dass Sie diese Typen immer noch bitkopieren können, solange andere wichtigere Änderungen nicht vorgenommen werden.


Da Sie nach Konstrukteuren gefragt haben: Nun, für diese sind die gleichen Dinge wahr. Beachten Sie, dass Konstruktoren auch implizite Aufrufe von Destruktoren enthalten. Bei Dingen wie auto_ptr machen diese Aufrufe (auch wenn sie zur Laufzeit nicht wirklich ausgeführt werden) die gleiche Gefahr wie für Destruktoren und passieren, wenn etwas im Konstruktor ausgelöst wird - der Compiler muss dann den Destruktor aufrufen der Mitglieder. This answer verwendet einige implizite Definition von Standardkonstruktoren.

Das gleiche gilt auch für Sichtbarkeit und PODness, die ich über den Destruktor oben gesagt habe.

Es gibt einen wichtigen Unterschied bei der Initialisierung. Wenn Sie einen vom Benutzer deklarierten Konstruktor setzen, erhält Ihr Typ keine Wertinitialisierung von Membern mehr, und es obliegt Ihrem Konstruktor, die erforderlichen Initialisierungen vorzunehmen. Beispiel:

struct A { 
    int a; 
}; 

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

In diesem Fall ist die folgende immer wahr

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

Während die folgende ist nicht definiertes Verhalten, weil b nie initialisiert wurde (Konstruktor weggelassen, dass). Der Wert kann Null sein, kann aber auch ein anderer seltsamer Wert sein. Der Versuch, von einem solchen nicht initialisierten Objekt zu lesen, führt zu undefiniertem Verhalten.

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

Dies gilt auch für die Verwendung dieser Syntax in new, wie new A() (beachten Sie die Klammern am Ende - wenn sie Wert Initialisierung weggelassen werden nicht durchgeführt wird, und da es keine Benutzer erklärt Konstruktor, der es nicht initialisieren , a wird nicht initialisiert).

+0

+1 für die Erwähnung von vorwärts deklarierten automatischen Zeigern und dem automatischen Destruktor. Ein gewöhnliches Problem, wenn Sie anfangen, Dinge zu deklarieren. –

+1

Ihr erstes Beispiel ist ein bisschen seltsam. Das B, das du geschrieben hast, kann überhaupt nicht verwendet werden (das neue B wäre ein Fehler, jede Umwandlung in ein wäre ein undefiniertes Verhalten, da es kein POD ist). –

+0

Auch A(). A == 0 gilt nur für die Statik. Eine lokale Variable vom Typ A wird nicht initialisiert. –

8

Ja, dieser leere Destruktor ist der selbe wie der automatisch generierte. Ich habe immer nur den Compiler automatisch generieren lassen; Ich denke nicht, dass es notwendig ist, den Destruktor explizit anzugeben, es sei denn, Sie müssen etwas Ungewöhnliches tun: machen Sie es virtuell oder privat, sagen Sie.

11

Der leere Destruktor, den Sie außerhalb der Klasse definiert haben, hat in den meisten Fällen eine ähnliche Semantik, aber nicht in allen.

Insbesondere wurde der implizit definiert destructor
1) ein inline öffentliches Element (dein ist inline nicht)
2) als trivial destructor bezeichnet (notwendig trivialen Typen zu machen, die in Vereinigungen sein können, dir nicht kann)
3) eine Ausnahmespezifikation (throw(), Ihnen nicht der Fall)

+1

Ein Hinweis zu 3: Die Ausnahmespezifikation ist in einem implizit definierten Destruktor nicht immer leer, wie in [except.spec] angegeben. – dalle

+0

@dalle +1 auf Kommentar - Danke für die Aufmerksamkeit darauf - Sie sind in der Tat richtig, wenn Foo hatte von Basisklassen jeweils mit nicht-impliziten Destruktoren mit Ausnahme-Spezifikationen abgeleitet - Foo impliziten dtor würde "geerbt" die Vereinigung dieser Ausnahmen Spezifikationen - in diesem Fall, da es keine Vererbung gibt, ist die Ausnahmespezifikation des impliziten dtors throw(). –

1

ich am besten würde sagen, die leere Erklärung zu setzen, ist es keine Zukunft Maintainer sagt, dass es nicht um ein Versehen war, und Sie wirklich wollte den Standard verwenden.

2

ich mit David, außer dass zustimmen würde ich sagen, es ist in der Regel eine gute Praxis, einen virtuellen Destruktor dh

virtual ~Foo() { } 

verpassten virtuellen Destruktor zu definieren, kann zu Speicherverlust führen, weil Menschen, die von Ihrer Foo-Klasse erben kann habe nicht bemerkt, dass ihr Destruktor niemals gerufen wird !!

0

Eine leere Definition ist in Ordnung, da die Definition kann

virtual ~GameManager();
noch lädt
virtual ~GameManager() { };
Die leere Erklärung ist täuschend ähnlich in Erscheinung verwiesen werden, um die gefürchtete keine Definition für virtuelle destructor Fehler
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

Ich weiß, ich bin zu spät in der Diskussion, aber meine Erfahrung sagt, dass der Compiler sich anders verhält, wenn er mit einem leeren Destruktor konfrontiert wird, verglichen mit einem vom Compiler erzeugten. Dies ist zumindest bei MSVC++ 8.0 (2005) und MSVC++ 9.0 (2008) der Fall.

Beim Betrachten der generierten Assembly für einige Code-Verwendung von Ausdruck Vorlagen, erkannte ich, dass im Freigabemodus, der Anruf an meine BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) wurde nie inline. (Bitte achten Sie nicht auf die genauen Typen und die Unterschrift des Betreibers).

Um das Problem weiter zu diagnostizieren, habe ich die verschiedenen Compiler Warnings That Are Off by Default aktiviert. Die Warnung C4714 ist besonders interessant. Es wird vom Compiler ausgegeben, wenn eine Funktion, die mit __forceinlinemarkiert ist, trotzdem nicht inline wird.

Ich habe die C4714-Warnung aktiviert und ich habe den Operator mit __forceinline markiert, und ich konnte die Compilerberichte überprüfen, dass es nicht möglich war, den Aufruf an den Operator zu inline zu schreiben.

Unter den Gründen, in der Dokumentation beschrieben, schlägt der Compiler eine Funktion mit __forceinline für markiert Inline:

Funktionen Rückkehr ein abwickelbar Objekt von Wert, wenn -GX/EHs/EHa auf

ist
Dies ist der Fall von meinem BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs). BinaryVectorExpression wird von Wert zurückgegeben und obwohl sein Destruktor leer ist, wird dieser Rückgabewert als ein deaktivierbares Objekt betrachtet. Das Hinzufügen von throw() zum Destruktor half dem Compiler und nicht. Wenn Sie den leeren Destruktor auskommentieren, kann der Compiler den Code vollständig einbinden.

Der Take-away ist, dass ich ab jetzt in jeder Klasse auskommentierte leere Destruktoren schreibe, damit die Menschen wissen, dass der Destruktor nichts absichtlich tut, genauso wie Leute die leere Ausnahmespezifikation `/ * throw () */um anzuzeigen, dass der Destruktor nicht werfen kann.

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

Hoffe, dass hilft.