Warum sollte ich einen virtuellen Destruktor für eine abstrakte Klasse in C ++ deklarieren?

165

Ich weiß, dass es eine gute Praxis ist, virtuelle Destruktoren für Basisklassen in C ++ zu deklarieren, aber ist es immer wichtig, virtualDestruktoren auch für abstrakte Klassen zu deklarieren , die als Schnittstellen fungieren? Bitte geben Sie einige Gründe und Beispiele an, warum.

Kevin
quelle

Antworten:

196

Es ist noch wichtiger für eine Schnittstelle. Jeder Benutzer Ihrer Klasse wird wahrscheinlich einen Zeiger auf die Schnittstelle halten, keinen Zeiger auf die konkrete Implementierung. Wenn sie zum Löschen kommen und der Destruktor nicht virtuell ist, rufen sie den Destruktor der Schnittstelle (oder den vom Compiler bereitgestellten Standard, falls Sie keinen angegeben haben) und nicht den Destruktor der abgeleiteten Klasse auf. Sofortiger Speicherverlust.

Beispielsweise

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}
Airsource Ltd.
quelle
4
delete pruft undefiniertes Verhalten auf. Es ist nicht garantiert anzurufen Interface::~Interface.
Mankarse
@Mankarse: Kannst du erklären, warum es undefiniert ist? Wenn Derived keinen eigenen Destruktor implementieren würde, wäre es dann immer noch undefiniertes Verhalten?
Ponkadoodle
14
@ Wallacoloo: Es ist undefiniert wegen [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Es wäre immer noch undefiniert, wenn Derived einen implizit generierten Destruktor verwenden würde.
Mankarse
37

Die Antwort auf Ihre Frage ist oft, aber nicht immer. Wenn Ihre abstrakte Klasse Clients verbietet, delete für einen Zeiger darauf aufzurufen (oder wenn dies in der Dokumentation angegeben ist), können Sie keinen virtuellen Destruktor deklarieren.

Sie können Clients verbieten, delete für einen Zeiger darauf aufzurufen, indem Sie dessen Destruktor schützen. Wenn Sie so arbeiten, ist es absolut sicher und vernünftig, einen virtuellen Destruktor wegzulassen.

Sie werden schließlich keine virtuelle Methodentabelle mehr haben und Ihren Clients Ihre Absicht signalisieren, sie durch einen Zeiger darauf nicht löschbar zu machen. Sie haben also in der Tat Grund, sie in diesen Fällen nicht als virtuell zu deklarieren.

[Siehe Punkt 4 in diesem Artikel: http://www.gotw.ca/publications/mill18.htm ]

Johannes Schaub - litb
quelle
Der Schlüssel, damit Ihre Antwort funktioniert, ist "bei dem das Löschen nicht erforderlich ist". Wenn Sie eine abstrakte Basisklasse haben, die als Schnittstelle konzipiert ist, wird normalerweise delete für die Schnittstellenklasse aufgerufen.
John Dibling
Wie John oben betont hat, ist das, was Sie vorschlagen, ziemlich gefährlich. Sie gehen davon aus, dass Clients Ihrer Schnittstelle niemals ein Objekt zerstören, das nur den Basistyp kennt. Die einzige Möglichkeit, dies zu garantieren, wenn es nicht virtuell ist, besteht darin, den dtor der abstrakten Klasse zu schützen.
Michel
Michel, ich habe es gesagt :) "Wenn Sie das tun, machen Sie Ihren Destruktor geschützt. Wenn Sie dies tun, können Clients nicht mit einem Zeiger auf diese Schnittstelle löschen." und in der Tat ist es nicht auf die Kunden angewiesen, sondern muss es erzwingen und den Kunden sagen "Sie können nicht ...". Ich sehe keine Gefahr
Johannes Schaub - litb
Ich habe jetzt den schlechten Wortlaut meiner Antwort korrigiert. es gibt es jetzt explizit an, da es nicht auf die Clients angewiesen ist. Eigentlich dachte ich, das ist offensichtlich, dass es sowieso nicht möglich ist, sich darauf zu verlassen, dass Kunden etwas tun. danke :)
Johannes Schaub - litb
2
+1 für die Erwähnung geschützter Destruktoren, die den anderen "Ausweg" aus dem Problem darstellen, versehentlich den falschen Destruktor aufzurufen, wenn ein Zeiger auf eine Basisklasse gelöscht wird.
j_random_hacker
23

Ich habe mich entschlossen, etwas zu recherchieren und zu versuchen, Ihre Antworten zusammenzufassen. Die folgenden Fragen helfen Ihnen bei der Entscheidung, welche Art von Destruktor Sie benötigen:

  1. Soll Ihre Klasse als Basisklasse verwendet werden?
    • Nein: Deklarieren Sie einen öffentlichen nicht virtuellen Destruktor, um einen V-Zeiger auf jedes Objekt der Klasse * zu vermeiden .
    • Ja: Nächste Frage lesen.
  2. Ist Ihre Basisklasse abstrakt? (dh irgendwelche virtuellen reinen Methoden?)
    • Nein: Versuchen Sie, Ihre Basisklasse abstrakt zu gestalten, indem Sie Ihre Klassenhierarchie neu gestalten
    • Ja: Nächste Frage lesen.
  3. Möchten Sie das polymorphe Löschen über einen Basiszeiger zulassen?
    • Nein: Deklarieren Sie einen geschützten virtuellen Destruktor, um die unerwünschte Verwendung zu verhindern.
    • Ja: Öffentlichen virtuellen Destruktor deklarieren (in diesem Fall kein Overhead).

Ich hoffe das hilft.

* Es ist wichtig zu beachten, dass es in C ++ keine Möglichkeit gibt, eine Klasse als endgültig (dh nicht unterklassifizierbar) zu markieren. Wenn Sie sich entscheiden, Ihren Destruktor als nicht virtuell und öffentlich zu deklarieren, denken Sie daran, Ihre Programmierkollegen ausdrücklich davor zu warnen von Ihrer Klasse abgeleitet.

Verweise:

Davidag
quelle
11
Diese Antwort ist teilweise veraltet, es gibt jetzt ein endgültiges Schlüsselwort in C ++.
Étienne
10

Ja, das ist immer wichtig. Abgeleitete Klassen können Speicher zuweisen oder Verweise auf andere Ressourcen enthalten, die bereinigt werden müssen, wenn das Objekt zerstört wird. Wenn Sie Ihren Schnittstellen / abstrakten Klassen keine virtuellen Destruktoren geben, wird jedes Mal, wenn Sie eine abgeleitete Klasseninstanz über ein Basisklassenhandle löschen, der Destruktor Ihrer abgeleiteten Klasse nicht aufgerufen.

Daher eröffnen Sie das Potenzial für Speicherlecks

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted
ABl.
quelle
In diesem Beispiel kann es zwar nicht nur zu einem Speicherverlust kommen, sondern möglicherweise auch zum Absturz: - /
Evan Teran
7

Es ist nicht immer erforderlich, aber ich finde es eine gute Praxis. Damit kann ein abgeleitetes Objekt sicher über einen Zeiger eines Basistyps gelöscht werden.

Also zum Beispiel:

Base *p = new Derived;
// use p as you see fit
delete p;

ist schlecht geformt, wenn Basekein virtueller Destruktor vorhanden ist, da versucht wird, das Objekt so zu löschen, als wäre es ein Base *.

Evan Teran
quelle
Möchten Sie nicht beheben, dass boost :: shared_pointer p (neu abgeleitet) so aussieht wie boost :: shared_pointer <Base> p (neu abgeleitet); ? Vielleicht wird ppl dann Ihre Antwort verstehen und abstimmen
Johannes Schaub - litb
EDIT: "Codiert" ein paar Teile, um die spitzen Klammern sichtbar zu machen, wie litb vorgeschlagen.
j_random_hacker
@EvanTeran: Ich bin nicht sicher, ob sich dies geändert hat, seit die Antwort ursprünglich veröffentlicht wurde (die Boost-Dokumentation unter boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm schlägt dies möglicherweise vor), aber es ist nicht wahr Heutzutage shared_ptrwird versucht, das Objekt so zu löschen, als wäre es ein Base *- es merkt sich den Typ des Objekts, mit dem Sie es erstellt haben. Siehe den Link, auf den verwiesen wird, insbesondere das Bit "Der Destruktor ruft delete mit demselben Zeiger auf, einschließlich seines ursprünglichen Typs, auch wenn T keinen virtuellen Destruktor hat oder ungültig ist."
Stuart Golodetz
@ StuartGolodetz: Hmm, Sie haben vielleicht Recht, aber ich bin mir ehrlich gesagt nicht sicher. In diesem Zusammenhang kann es aufgrund des Mangels an virtuellem Destruktor immer noch schlecht geformt sein . Es lohnt sich, durchzusehen.
Evan Teran
@EvanTeran: Falls es hilfreich ist - stackoverflow.com/questions/3899790/shared-ptr-magic .
Stuart Golodetz
5

Es ist nicht nur eine gute Praxis. Es ist Regel Nr. 1 für jede Klassenhierarchie.

  1. Die Basisklasse einer Hierarchie in C ++ muss einen virtuellen Destruktor haben

Nun zum Warum. Nehmen Sie die typische Tierhierarchie. Virtuelle Destruktoren durchlaufen den virtuellen Versand wie jeder andere Methodenaufruf. Nehmen Sie das folgende Beispiel.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Angenommen, Animal ist eine abstrakte Klasse. C ++ kennt den richtigen Destruktor nur über den Versand virtueller Methoden. Wenn der Destruktor nicht virtuell ist, ruft er einfach den Destruktor von Animal auf und zerstört keine Objekte in abgeleiteten Klassen.

Der Grund dafür, den Destruktor in der Basisklasse virtuell zu machen, besteht darin, dass die Auswahl einfach aus abgeleiteten Klassen entfernt wird. Ihr Destruktor wird standardmäßig virtuell.

JaredPar
quelle
2
Ich stimme Ihnen größtenteils zu, da Sie normalerweise beim Definieren einer Hierarchie mit einem Basisklassenzeiger / einer Referenz auf ein abgeleitetes Objekt verweisen möchten. Dies ist jedoch nicht immer der Fall, und in diesen anderen Fällen kann es ausreichen, die Basisklasse dtor stattdessen zu schützen.
j_random_hacker
@j_random_hacker, der es schützt, schützt Sie nicht vor falschen internen Löschungen
JaredPar
1
@JaredPar: Das stimmt, aber zumindest können Sie in Ihrem eigenen Code verantwortlich sein - das Schwierige ist, sicherzustellen, dass Client-Code nicht dazu führen kann, dass Ihr Code explodiert. (In ähnlicher Weise hindert das
Privatisieren
@j_random_hacker, es tut mir leid, mit einem Blog-Beitrag zu antworten, aber es passt wirklich zu diesem Szenario. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar
@JaredPar: Ausgezeichneter Beitrag, ich stimme Ihnen zu 100% zu, insbesondere in Bezug auf die Überprüfung von Verträgen im Einzelhandelscode. Ich meine nur, dass es Fälle gibt, in denen Sie wissen, dass Sie keinen virtuellen dtor benötigen. Beispiel: Tag-Klassen für den Vorlagenversand. Sie haben die Größe 0, Sie verwenden die Vererbung nur, um Spezialisierungen anzuzeigen.
j_random_hacker
3

Die Antwort ist einfach: Sie müssen virtuell sein, sonst wäre die Basisklasse keine vollständige polymorphe Klasse.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

Sie würden das obige Löschen bevorzugen, aber wenn der Destruktor der Basisklasse nicht virtuell ist, wird nur der Destruktor der Basisklasse aufgerufen und alle Daten in der abgeleiteten Klasse bleiben nicht gelöscht.

fatma.ekici
quelle