Gibt es jemals einen guten Grund , einen virtuellen Destruktor für eine Klasse nicht zu deklarieren? Wann sollten Sie es ausdrücklich vermeiden, eine zu schreiben?
Gibt es jemals einen guten Grund , einen virtuellen Destruktor für eine Klasse nicht zu deklarieren? Wann sollten Sie es ausdrücklich vermeiden, eine zu schreiben?
Antworten:
Es ist nicht erforderlich, einen virtuellen Destruktor zu verwenden, wenn eine der folgenden Bedingungen erfüllt ist:
Kein besonderer Grund, dies zu vermeiden, es sei denn, Sie sind wirklich so gedrängt.
Um die Frage explizit zu beantworten, dh wann sollten Sie keinen virtuellen Destruktor deklarieren.
C ++ '98 / '03
Durch Hinzufügen eines virtuellen Destruktors wird Ihre Klasse möglicherweise von POD (einfache alte Daten) * oder Aggregat zu Nicht-POD geändert. Dies kann die Kompilierung Ihres Projekts verhindern, wenn Ihr Klassentyp irgendwo aggregiert initialisiert wird.
struct A {
// virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // Will fail if virtual dtor declared
}
Im Extremfall kann eine solche Änderung auch zu undefiniertem Verhalten führen, wenn die Klasse auf eine Weise verwendet wird, die einen POD erfordert, z. B. über einen Auslassungsparameter übergeben oder mit memcpy verwendet wird.
void bar (...);
void foo (A & a) {
bar (a); // Undefined behavior if virtual dtor declared
}
[* Ein POD-Typ ist ein Typ, der spezifische Garantien für sein Speicherlayout hat. Der Standard besagt wirklich nur, dass das Ergebnis das gleiche ist wie das ursprüngliche Objekt, wenn Sie von einem Objekt mit POD-Typ in ein Array von Zeichen (oder Zeichen ohne Vorzeichen) und wieder zurück kopieren.]
Modernes C ++
In neueren Versionen von C ++ wurde das POD-Konzept zwischen dem Klassenlayout und seiner Konstruktion, dem Kopieren und der Zerstörung aufgeteilt.
Für den Fall der Auslassungspunkte handelt es sich nicht mehr um ein undefiniertes Verhalten, das jetzt mit implementierungsdefinierter Semantik (N3937 - ~ C ++ '14 - 5.2.2 / 7) bedingt unterstützt wird:
... Das Übergeben eines potenziell ausgewerteten Arguments vom Klassentyp (Abschnitt 9) mit einem nicht trivialen Kopierkonstruktor, einem nicht trivialen Verschiebungskonstruktor oder einem trivialen Destruktor ohne entsprechenden Parameter wird bei der Implementierung bedingt unterstützt. definierte Semantik.
Wenn Sie einen anderen Destruktor als =default
deklarieren, bedeutet dies, dass dies nicht trivial ist (12.4 / 5).
... Ein Destruktor ist trivial, wenn er nicht vom Benutzer bereitgestellt wird ...
Andere Änderungen an Modern C ++ verringern die Auswirkungen des Problems der aggregierten Initialisierung, da ein Konstruktor hinzugefügt werden kann:
struct A {
A(int i, int j);
virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // OK
}
Ich deklariere einen virtuellen Destruktor genau dann, wenn ich virtuelle Methoden habe. Sobald ich virtuelle Methoden habe, vertraue ich mir nicht darauf, diese auf dem Heap zu instanziieren oder einen Zeiger auf die Basisklasse zu speichern. Beides sind äußerst häufige Vorgänge, bei denen häufig stillschweigend Ressourcen verloren gehen, wenn der Destruktor nicht als virtuell deklariert wird.
Ein virtueller Destruktor wird immer dann benötigt, wenn die Möglichkeit besteht, dass delete
ein Zeiger auf ein Objekt einer Unterklasse mit dem Typ Ihrer Klasse aufgerufen wird. Dadurch wird sichergestellt, dass der richtige Destruktor zur Laufzeit aufgerufen wird, ohne dass der Compiler zur Kompilierungszeit die Klasse eines Objekts auf dem Heap kennen muss. Angenommen, es B
handelt sich um eine Unterklasse von A
:
A *x = new B;
delete x; // ~B() called, even though x has type A*
Wenn Ihr Code nicht leistungskritisch ist, ist es aus Sicherheitsgründen sinnvoll, jeder Basisklasse, die Sie schreiben, einen virtuellen Destruktor hinzuzufügen.
Wenn Sie jedoch feststellen, dass sich delete
viele Objekte in einer engen Schleife befinden, kann der Leistungsaufwand beim Aufrufen einer virtuellen Funktion (auch einer leeren) spürbar sein. Der Compiler kann diese Aufrufe normalerweise nicht einbinden, und der Prozessor hat möglicherweise Schwierigkeiten, vorherzusagen, wohin er gehen soll. Es ist unwahrscheinlich, dass dies einen signifikanten Einfluss auf die Leistung hat, aber es ist erwähnenswert.
Virtuelle Funktionen bedeuten, dass jedes zugewiesene Objekt die Speicherkosten durch einen virtuellen Funktionstabellenzeiger erhöht.
Wenn Ihr Programm also eine sehr große Anzahl von Objekten zuweist, sollten Sie alle virtuellen Funktionen vermeiden, um die zusätzlichen 32 Bit pro Objekt zu speichern.
In allen anderen Fällen ersparen Sie sich das Debug-Elend, um den dtor virtuell zu machen.
Nicht alle C ++ - Klassen eignen sich zur Verwendung als Basisklasse mit dynamischem Polymorphismus.
Wenn Ihre Klasse für dynamischen Polymorphismus geeignet sein soll, muss ihr Destruktor virtuell sein. Darüber hinaus müssen alle Methoden, die eine Unterklasse möglicherweise überschreiben möchte (was bedeuten könnte, dass alle öffentlichen Methoden sowie möglicherweise einige geschützte Methoden, die intern verwendet werden), virtuell sein.
Wenn Ihre Klasse nicht für dynamischen Polymorphismus geeignet ist, sollte der Destruktor nicht als virtuell markiert werden, da dies irreführend ist. Es ermutigt die Leute nur, Ihre Klasse falsch zu benutzen.
Hier ist ein Beispiel für eine Klasse, die für einen dynamischen Polymorphismus nicht geeignet wäre, selbst wenn ihr Destruktor virtuell wäre:
class MutexLock {
mutex *mtx_;
public:
explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
~MutexLock() { mtx_->unlock(); }
private:
MutexLock(const MutexLock &rhs);
MutexLock &operator=(const MutexLock &rhs);
};
Der springende Punkt dieser Klasse ist, für RAII auf dem Stapel zu sitzen. Wenn Sie Zeiger auf Objekte dieser Klasse weitergeben, geschweige denn Unterklassen davon, dann machen Sie es falsch.
Ein guter Grund, einen Destruktor nicht als virtuell zu deklarieren, besteht darin, dass Ihre Klasse dadurch nicht vor dem Hinzufügen einer virtuellen Funktionstabelle geschützt wird. Dies sollten Sie nach Möglichkeit vermeiden.
Ich weiß, dass viele Leute es vorziehen, Destruktoren einfach immer als virtuell zu deklarieren, nur um auf der sicheren Seite zu sein. Aber wenn Ihre Klasse keine anderen virtuellen Funktionen hat, macht es wirklich keinen Sinn, einen virtuellen Destruktor zu haben. Selbst wenn Sie Ihre Klasse anderen Personen geben, die dann andere Klassen daraus ableiten, hätten sie keinen Grund, jemals delete für einen Zeiger aufzurufen, der auf Ihre Klasse übertragen wurde - und wenn dies der Fall ist, würde ich dies als Fehler betrachten.
Okay, es gibt eine einzige Ausnahme: Wenn Ihre Klasse (falsch) zum polymorphen Löschen abgeleiteter Objekte verwendet wird, wissen Sie - oder die anderen - hoffentlich, dass dies einen virtuellen Destruktor erfordert.
Anders ausgedrückt, wenn Ihre Klasse einen nicht virtuellen Destruktor hat, ist dies eine sehr klare Aussage: "Verwenden Sie mich nicht zum Löschen abgeleiteter Objekte!"
Wenn Sie eine sehr kleine Klasse mit einer großen Anzahl von Instanzen haben, kann der Overhead eines vtable-Zeigers die Speichernutzung Ihres Programms beeinflussen. Solange Ihre Klasse keine anderen virtuellen Methoden hat, wird dieser Overhead gespart, wenn der Destruktor nicht virtuell ist.
Normalerweise deklariere ich den Destruktor als virtuell, aber wenn Sie leistungskritischen Code haben, der in einer inneren Schleife verwendet wird, möchten Sie möglicherweise die Suche nach virtuellen Tabellen vermeiden. Dies kann in einigen Fällen wichtig sein, z. B. bei der Kollisionsprüfung. Aber seien Sie vorsichtig, wie Sie diese Objekte zerstören, wenn Sie Vererbung verwenden, oder Sie zerstören nur die Hälfte des Objekts.
Beachten Sie, dass die Suche nach virtuellen Tabellen für ein Objekt erfolgt, wenn eine Methode für dieses Objekt virtuell ist. Es macht also keinen Sinn, die virtuelle Spezifikation auf einem Destruktor zu entfernen, wenn Sie andere virtuelle Methoden in der Klasse haben.
Wenn Sie unbedingt sicherstellen müssen, dass Ihre Klasse keine vtable hat, dürfen Sie auch keinen virtuellen Destruktor haben.
Dies ist ein seltener Fall, aber es kommt vor.
Das bekannteste Beispiel für ein Muster, das dies tut, sind die Klassen DirectX D3DVECTOR und D3DMATRIX. Dies sind Klassenmethoden anstelle von Funktionen für den syntaktischen Zucker, aber die Klassen haben absichtlich keine vtable, um den Funktionsaufwand zu vermeiden, da diese Klassen speziell in der inneren Schleife vieler Hochleistungsanwendungen verwendet werden.
Ein Vorgang, der für die Basisklasse ausgeführt wird und der sich virtuell verhalten sollte, sollte virtuell sein. Wenn das Löschen polymorph über die Basisklassenschnittstelle durchgeführt werden kann, muss es sich virtuell verhalten und virtuell sein.
Der Destruktor muss nicht virtuell sein, wenn Sie nicht beabsichtigen, von der Klasse abzuleiten. Und selbst wenn Sie dies tun, ist ein geschützter nicht virtueller Destruktor genauso gut, wenn das Löschen von Basisklassenzeigern nicht erforderlich ist .
Die Performance-Antwort ist die einzige, von der ich weiß, dass sie eine Chance hat, wahr zu sein. Wenn Sie gemessen und festgestellt haben, dass die De-Virtualisierung Ihrer Destruktoren die Dinge wirklich beschleunigt, dann haben Sie wahrscheinlich andere Dinge in dieser Klasse, die ebenfalls beschleunigt werden müssen, aber an diesem Punkt gibt es wichtigere Überlegungen. Eines Tages wird jemand feststellen, dass Ihr Code eine nette Basisklasse für ihn darstellt und ihm eine Woche Arbeit erspart. Stellen Sie besser sicher, dass sie die Arbeit dieser Woche erledigen, Ihren Code kopieren und einfügen, anstatt Ihren Code als Basis zu verwenden. Sie sollten sicherstellen, dass Sie einige Ihrer wichtigen Methoden privat machen, damit niemand jemals von Ihnen erben kann.