Wann sollten Sie keine virtuellen Destruktoren verwenden?

98

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?

Mag Roader
quelle

Antworten:

72

Es ist nicht erforderlich, einen virtuellen Destruktor zu verwenden, wenn eine der folgenden Bedingungen erfüllt ist:

  • Keine Absicht, daraus Klassen abzuleiten
  • Keine Instanziierung auf dem Haufen
  • Keine Absicht, in einem Zeiger einer Oberklasse zu speichern

Kein besonderer Grund, dies zu vermeiden, es sei denn, Sie sind wirklich so gedrängt.

sep
quelle
25
Dies ist keine gute Antwort. "Es gibt keine Notwendigkeit" unterscheidet sich von "sollte nicht", und "keine Absicht" unterscheidet sich von "unmöglich gemacht".
Windows-Programmierer
5
Fügen Sie außerdem hinzu: Keine Absicht, eine Instanz über einen Basisklassenzeiger zu löschen.
Adam Rosenfield
9
Dies beantwortet die Frage nicht wirklich. Wo ist Ihr guter Grund, keinen virtuellen dtor zu verwenden?
mxcl
9
Ich denke, wenn es nicht nötig ist, etwas zu tun, ist das ein guter Grund, es nicht zu tun. Es folgt dem Simple Design-Prinzip von XP.
sep
12
Wenn Sie sagen, dass Sie "keine Absicht" haben, machen Sie eine große Annahme darüber, wie Ihre Klasse verwendet wird. Es scheint mir, dass die einfachste Lösung in den meisten Fällen (die daher die Standardeinstellung sein sollte) darin bestehen sollte, virtuelle Destruktoren zu haben und diese nur zu vermeiden, wenn Sie einen bestimmten Grund haben, dies nicht zu tun. Ich bin also immer noch gespannt, was ein guter Grund wäre.
Ckarras
68

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 =defaultdeklarieren, 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
}
Richard Corden
quelle
1
Sie haben Recht, und ich habe mich geirrt, Leistung ist nicht der einzige Grund. Aber das zeigt, dass ich im Rest Recht hatte: Der Programmierer der Klasse sollte besser Code einschließen, um zu verhindern, dass die Klasse jemals von jemand anderem geerbt wird.
Windows-Programmierer
Lieber Richard, kannst du bitte etwas mehr zu dem kommentieren, was du geschrieben hast? Ich verstehe Ihren Punkt nicht, aber es scheint der einzige wertvolle Punkt zu sein, den ich durch Googeln gefunden habe.) Oder können Sie einen Link zu einer detaillierteren Erklärung geben?
John Smith
1
@ JohnSmith Ich habe die Antwort aktualisiert. Hoffentlich hilft das.
Richard Corden
27

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.

Andy
quelle
3
Tatsächlich gibt es auf gcc eine Warnoption, die genau in diesem Fall warnt (virtuelle Methoden, aber kein virtueller dtor).
CesarB
6
Gehen Sie dann nicht das Risiko ein, Speicher zu verlieren, wenn Sie von der Klasse abgeleitet sind, unabhängig davon, ob Sie andere virtuelle Funktionen haben?
Mag Roader
1
Ich stimme mit mag. Diese Verwendung eines virtuellen Destruktors und / oder einer virtuellen Methode sind separate Anforderungen. Der virtuelle Destruktor bietet einer Klasse die Möglichkeit, eine Bereinigung durchzuführen (z. B. Speicher löschen, Dateien schließen usw.) UND stellt außerdem sicher, dass die Konstruktoren aller ihrer Mitglieder aufgerufen werden.
user48956
7

Ein virtueller Destruktor wird immer dann benötigt, wenn die Möglichkeit besteht, dass deleteein 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 Bhandelt 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 deleteviele 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.

Jay Conrod
quelle
"Wenn Ihr Code nicht leistungskritisch ist, ist es aus Sicherheitsgründen sinnvoll, jeder Basisklasse, die Sie schreiben, einen virtuellen Destruktor hinzuzufügen." sollte in jeder Antwort, die ich sehe, mehr betont werden
csguy
5

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.

mxcl
quelle
1
Nur ein Trottel, aber heutzutage ist ein Zeiger oft 64 Bit statt 32.
Head Geek
5

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.

Steve Jessop
quelle
2
Polymorphe Verwendung bedeutet keine polymorphe Löschung. Es gibt viele Anwendungsfälle für eine Klasse, in denen virtuelle Methoden vorhanden sind, jedoch kein virtueller Destruktor. Stellen Sie sich ein typisches statisch definiertes Dialogfeld in so ziemlich jedem GUI-Toolkit vor. Das übergeordnete Fenster zerstört die untergeordneten Objekte und kennt den genauen Typ der einzelnen Objekte. Alle untergeordneten Fenster werden jedoch auch an einer beliebigen Anzahl von Stellen polymorph verwendet, z. B. beim Testen von Treffern, Zeichnen und Eingabehilfen-APIs, mit denen der Text für Text abgerufen wird.
Ben Voigt
4
Stimmt, aber der Fragesteller fragt, wann Sie einen virtuellen Destruktor gezielt vermeiden sollten. Für das von Ihnen beschriebene Dialogfeld ist ein virtueller Destruktor sinnlos, aber IMO nicht schädlich. Ich bin mir nicht sicher, ob ich sicher sein kann, dass ich niemals ein Dialogfeld mit einem Basisklassenzeiger löschen muss. Beispielsweise möchte ich möglicherweise in Zukunft, dass mein übergeordnetes Fenster seine untergeordneten Objekte mithilfe von Fabriken erstellt. Es geht also nicht darum , einen virtuellen Destruktor zu vermeiden , sondern nur darum, dass Sie sich nicht darum kümmern, einen zu haben. Ein virtueller Destruktor in einer Klasse, die nicht zur Ableitung geeignet ist , ist jedoch schädlich, da er irreführend ist.
Steve Jessop
4

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!"

Kidfisto
quelle
3

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.

Mark Ransom
quelle
1

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.

Jørn Jensen
quelle
1

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.

Lisa
quelle
0

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 .

Eiskriminalität
quelle
-7

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.

Windows-Programmierer
quelle
Polymorphismus wird die Dinge sicherlich verlangsamen. Vergleichen Sie es mit einer Situation, in der wir Polymorphismus brauchen und entscheiden Sie sich dagegen, es wird noch langsamer. Beispiel: Wir implementieren die gesamte Logik im Basisklassen-Destruktor mithilfe von RTTI und einer switch-Anweisung, um Ressourcen zu bereinigen.
sep
1
In C ++ liegt es nicht in Ihrer Verantwortung, mich daran zu hindern, von Ihren Klassen zu erben, von denen Sie dokumentiert haben, dass sie nicht für die Verwendung als Basisklassen geeignet sind. Es liegt in meiner Verantwortung, die Vererbung mit Vorsicht anzuwenden. Sofern der House Style Guide nichts anderes sagt, natürlich.
Steve Jessop
1
... nur den Destruktor virtuell zu machen, bedeutet nicht, dass die Klasse als Basisklasse korrekt funktioniert. Wenn Sie es also virtuell "nur weil" markieren, anstatt diese Bewertung vorzunehmen, schreiben Sie einen Scheck, den mein Code nicht einlösen kann.
Steve Jessop