Ist das manuelle Aufrufen des Destruktors immer ein Zeichen für schlechtes Design?

80

Ich dachte: Sie sagen, wenn Sie den Destruktor manuell aufrufen, machen Sie etwas falsch. Aber ist das immer so? Gibt es Gegenbeispiele? Situationen, in denen es notwendig ist, es manuell aufzurufen, oder in denen es schwierig / unmöglich / unpraktisch ist, es zu vermeiden?

Violette Giraffe
quelle
Wie können Sie die Zuordnung des Objekts nach dem Aufrufen des dtor aufheben, ohne es erneut aufzurufen?
ssube
2
@peachykeen: Sie würden die Platzierung aufrufen new, um ein neues Objekt anstelle des alten zu initialisieren. Im Allgemeinen keine gute Idee, aber es ist nicht ungewöhnlich.
D. Shawley
14
Schauen Sie sich "Regeln" an, die die Wörter "immer" und "nie" enthalten, die nicht direkt aus Spezifikationen mit Verdächtigen stammen: In den meisten Fällen möchte derjenige, der sie unterrichtet, Ihnen Dinge verbergen, die Sie wissen sollten, aber er tut es nicht weiß, wie man unterrichtet. Genau wie ein Erwachsener, der einem Kind eine Frage zum Thema Sex beantwortet.
Emilio Garavaglia
Ich denke, es ist in Ordnung, wenn Objekte mit Platzierungstechnik manipuliert werden. Stroustrup.com/bs_faq2.html#placement-delete (aber es ist eher eine Sache auf niedriger Ebene und wird nur verwendet, wenn Sie Ihre Software auch in einer solchen Ebene optimieren)
bruziuz

Antworten:

91

Das manuelle Aufrufen des Destruktors ist erforderlich, wenn das Objekt mit einer überladenen Form von erstellt wurde operator new(), außer bei Verwendung der " std::nothrow" Überladungen:

T* t0 = new(std::nothrow) T();
delete t0; // OK: std::nothrow overload

void* buffer = malloc(sizeof(T));
T* t1 = new(buffer) T();
t1->~T(); // required: delete t1 would be wrong
free(buffer);

Außerhalb Speicherverwaltung auf einem eher niedrigen Niveau wie oben Destruktoren explizit, jedoch fordern, ist ein Zeichen für schlechtes Design. Wahrscheinlich ist es nicht nur schlechtes Design, sondern völlig falsch (ja, die Verwendung eines expliziten Destruktors, gefolgt von einem Aufruf eines Kopierkonstruktors im Zuweisungsoperator, ist ein schlechtes Design und wahrscheinlich falsch).

In C ++ 2011 gibt es einen weiteren Grund für die Verwendung expliziter Destruktoraufrufe: Bei Verwendung von verallgemeinerten Vereinigungen muss das aktuelle Objekt explizit zerstört und ein neues Objekt mithilfe der Platzierung new erstellt werden, wenn der Typ des dargestellten Objekts geändert wird. Wenn die Vereinigung zerstört wird, muss der Destruktor des aktuellen Objekts explizit aufgerufen werden, wenn eine Zerstörung erforderlich ist.

Dietmar Kühl
quelle
25
Anstatt "Verwenden einer überladenen Form von operator new" zu sagen , lautet der korrekte Ausdruck "Verwenden placement new".
Remy Lebeau
5
@RemyLebeau: Nun, ich wollte klarstellen, dass ich nicht nur über operator new(std::size_t, void*)(und die Array-Variation) spreche , sondern über alle überladenen Versionen von operator new().
Dietmar Kühl
Was ist, wenn Sie ein Objekt kopieren möchten, um eine Operation darin auszuführen, ohne es zu ändern, während die Operation berechnet wird? temp = Class(object); temp.operation(); object.~Class(); object = Class(temp); temp.~Class();
Jean-Luc Nacif Coelho
yes, using an explicit destructor followed by a copy constructor call in the assignment operator is a bad design and likely to be wrong. Warum sagst du das? Ich würde denken, wenn der Destruktor trivial oder nahezu trivial ist, hat er nur minimalen Overhead und erhöht die Verwendung des DRY-Prinzips. Wenn es in solchen Fällen mit einem Zug verwendet wird operator=(), ist es möglicherweise sogar besser als die Verwendung eines Swaps. YMMV.
Adrian
1
@Adrian: Wenn Sie den Destruktor aufrufen und das Objekt neu erstellen, ändert sich der Objekttyp sehr einfach. Es wird ein Objekt mit dem statischen Typ der Zuweisung neu erstellt, der dynamische Typ kann jedoch unterschiedlich sein. Dies ist tatsächlich ein Problem, wenn die Klasse virtualFunktionen hat (die virtualFunktionen werden nicht neu erstellt) und das Objekt ansonsten nur teilweise [neu] konstruiert wird.
Dietmar Kühl
99

Alle Antworten beschreiben bestimmte Fälle, aber es gibt eine allgemeine Antwort:

Sie rufen den dtor jedes Mal explizit auf, wenn Sie das Objekt nur zerstören müssen (im C ++ - Sinne), ohne den Speicher freizugeben, in dem sich das Objekt befindet.

Dies geschieht normalerweise in allen Situationen, in denen die Speicherzuweisung / -freigabe unabhängig von der Objektkonstruktion / -zerstörung verwaltet wird. In diesen Fällen erfolgt die Konstruktion durch Platzierung neu auf einem vorhandenen Speicherblock, und die Zerstörung erfolgt durch expliziten dtor-Aufruf.

Hier ist das rohe Beispiel:

{
  char buffer[sizeof(MyClass)];

  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }
  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }

}

Ein weiteres bemerkenswertes Beispiel ist die Standardeinstellung std::allocatorbei Verwendung durch std::vector: Elemente werden vectorwährend erstellt push_back, aber der Speicher wird in Blöcken zugewiesen, sodass die Elementkonstruktion bereits vorhanden ist. Und daher vector::erasemüssen die Elemente zerstört werden, aber nicht unbedingt wird der Speicher freigegeben (insbesondere, wenn bald ein neuer push_back erfolgen muss ...).

Es ist "schlechtes Design" im strengen OOP-Sinne (Sie sollten Objekte verwalten, nicht Speicher: Die Tatsache, dass Objekte Speicher benötigen, ist ein "Vorfall"), es ist "gutes Design" in "Low-Level-Programmierung" oder in Fällen, in denen Speicher vorhanden ist Nicht aus dem "Free Store" entnommen, operator newkauft der Standard ein.

Es ist schlechtes Design, wenn es zufällig um den Code herum passiert, es ist gutes Design, wenn es lokal für Klassen passiert, die speziell für diesen Zweck entwickelt wurden.

Emilio Garavaglia
quelle
8
Nur neugierig, warum dies nicht die akzeptierte Antwort ist.
Francis Cugler
11

Nein, Sie sollten es nicht explizit aufrufen, da es zweimal aufgerufen würde. Einmal für den manuellen Aufruf und ein anderes Mal, wenn der Bereich, in dem das Objekt deklariert ist, endet.

Z.B.

{
  Class c;
  c.~Class();
}

Wenn Sie wirklich dieselben Vorgänge ausführen müssen, sollten Sie eine separate Methode haben.

Es gibt eine bestimmte Situation, in der Sie möglicherweise einen Destruktor für ein dynamisch zugewiesenes Objekt mit einer Platzierung aufrufen möchten, newaber es klingt nicht nach etwas, das Sie jemals benötigen werden.

Jack
quelle
11

Nein, hängt von der Situation ab, manchmal ist es legitim und gutes Design.

Um zu verstehen, warum und wann Sie Destruktoren explizit aufrufen müssen, schauen wir uns an, was mit "neu" und "löschen" passiert.

So erstellen Sie ein Objekt dynamisch T* t = new T;unter der Haube: 1. Größe des (T) Speichers wird zugewiesen. 2. Der Konstruktor von T wird aufgerufen, um den zugewiesenen Speicher zu initialisieren. Der Operator new macht zwei Dinge: Zuweisung und Initialisierung.

So zerstören Sie das Objekt delete t;unter der Haube: 1. Der Destruktor von T wird aufgerufen. 2. Der für dieses Objekt zugewiesene Speicher wird freigegeben. Der Operator delete führt außerdem zwei Aktionen aus: Zerstörung und Freigabe.

Man schreibt den Konstruktor, um die Initialisierung durchzuführen, und den Destruktor, um die Zerstörung durchzuführen. Wenn Sie den Destruktor explizit aufrufen, erfolgt nur die Zerstörung, nicht jedoch die Freigabe .

Eine legitime Verwendung des expliziten Aufrufs des Destruktors könnte daher sein: "Ich möchte nur das Objekt zerstören, aber ich kann (oder kann) die Speicherzuordnung (noch) nicht freigeben."

Ein häufiges Beispiel hierfür ist die Vorbelegung des Speichers für einen Pool bestimmter Objekte, die ansonsten dynamisch zugewiesen werden müssen.

Wenn Sie ein neues Objekt erstellen, erhalten Sie den Speicherblock aus dem vorab zugewiesenen Pool und führen eine "Platzierung neu" durch. Nachdem Sie mit dem Objekt fertig sind, möchten Sie möglicherweise den Destruktor explizit aufrufen, um die Bereinigungsarbeiten abzuschließen, falls vorhanden. Sie werden den Speicher jedoch nicht freigeben, wie dies beim Löschen durch den Operator der Fall gewesen wäre. Stattdessen geben Sie den Block zur Wiederverwendung in den Pool zurück.


quelle
6

Jedes Mal, wenn Sie die Zuordnung von der Initialisierung trennen müssen, müssen Sie den Destruktor manuell neu und explizit aufrufen. Heutzutage ist dies selten erforderlich, da wir die Standardcontainer haben. Wenn Sie jedoch eine neue Art von Container implementieren müssen, benötigen Sie diese.

James Kanze
quelle
3

Es gibt Fälle, in denen sie notwendig sind:

In Code, an dem ich arbeite, verwende ich einen expliziten Destruktoraufruf in Allokatoren. Ich habe eine Implementierung eines einfachen Allokators, der die Platzierung new verwendet, um Speicherblöcke an stl-Container zurückzugeben. In zerstören habe ich:

  void destroy (pointer p) {
    // destroy objects by calling their destructor
    p->~T();
  }

während im Konstrukt:

  void construct (pointer p, const T& value) {
    // initialize memory with placement new
    #undef new
    ::new((PVOID)p) T(value);
  }

Die Zuweisung erfolgt auch in allocate () und die Speicherfreigabe in deallocate () unter Verwendung plattformspezifischer Zuweisungs- und Freigabemechanismen. Dieser Allokator wurde verwendet, um Doug Lea Malloc zu umgehen und direkt beispielsweise LocalAlloc unter Windows zu verwenden.

marcinj
quelle
1

Ich habe 3 Gelegenheiten gefunden, bei denen ich dies tun musste:

  • Zuweisen / Freigeben von Objekten im Speicher, die durch Speicherzuordnung oder gemeinsam genutzten Speicher erstellt wurden
  • bei der Implementierung einer bestimmten C-Schnittstelle mit C ++ (ja, das passiert leider heute noch (weil ich nicht genug Einfluss habe, um es zu ändern))
  • bei der Implementierung von Allokatorklassen

quelle
1

Ich bin noch nie auf eine Situation gestoßen, in der man einen Destruktor manuell aufrufen muss. Ich scheine mich zu erinnern, dass sogar Stroustrup behauptet, es sei eine schlechte Praxis.

Lieuwe
quelle
1
Du hast Recht. Aber ich habe eine neue Platzierung verwendet. Ich konnte die Bereinigungsfunktionalität in einer anderen Methode als dem Destruktor hinzufügen. Der Destruktor ist da, so dass er "automatisch" aufgerufen werden kann, wenn einer gelöscht wird. Wenn Sie manuell zerstören, aber nicht freigeben möchten, können Sie einfach ein "onDestruct" schreiben, nicht wahr? Es würde mich interessieren zu hören, ob es Beispiele gibt, bei denen ein Objekt seine Zerstörung im Destruktor durchführen müsste, weil Sie manchmal löschen müssten und manchmal nur zerstören und nicht freigeben möchten.
Lieuwe
Und selbst in diesem Fall könnten Sie onDestruct () aus dem Destruktor heraus aufrufen - daher sehe ich immer noch keinen Fall für das manuelle Aufrufen des Destruktors.
Lieuwe
4
@ JimBalter: Schöpfer von C+
Mark K Cowan
@ MarkKCowan: Was ist C +? Es sollte C ++
Destructor
1

Was ist damit?
Der Destruktor wird nicht aufgerufen, wenn eine Ausnahme vom Konstruktor ausgelöst wird. Daher muss ich ihn manuell aufrufen, um Handles zu zerstören, die vor der Ausnahme im Konstruktor erstellt wurden.

class MyClass {
  HANDLE h1,h2;
  public:
  MyClass() {
    // handles have to be created first
    h1=SomeAPIToCreateA();
    h2=SomeAPIToCreateB();        
    try {
      ...
      if(error) {
        throw MyException();
      }
    }
    catch(...) {
      this->~MyClass();
      throw;
    }
  }
  ~MyClass() {
    SomeAPIToDestroyA(h1);
    SomeAPIToDestroyB(h2);
  }
};
CITBL
quelle
1
Dies scheint fraglich: Wenn Ihr Konstruktor trowst, wissen Sie nicht (oder wissen möglicherweise nicht), welche Teile des Objekts konstruiert wurden und welche nicht. Sie wissen also nicht, für welche Unterobjekte Sie beispielsweise Destruktoren aufrufen sollen. Oder welche der vom Konstruktor zugewiesenen Ressourcen freigegeben werden sollen.
Violette Giraffe
@VioletGiraffe Wenn die Unterobjekte auf einem Stapel erstellt werden, dh nicht mit "neu", werden sie automatisch zerstört. Andernfalls können Sie überprüfen, ob sie NULL sind, bevor Sie sie im Destruktor zerstören. Gleiches gilt für die Ressourcen
CITBL
Die Art und Weise, wie Sie das ctorhier geschrieben haben, ist falsch, genau aus dem Grund, den Sie selbst angegeben haben: Wenn die Ressourcenzuweisung fehlschlägt, liegt ein Problem bei der Bereinigung vor. Ein 'ctor' sollte nicht anrufen this->~dtor(). dtorsollte für konstruierte Objekte aufgerufen werden, und in diesem Fall ist das Objekt noch nicht konstruiert. Was auch immer passiert, das ctorsollte die Bereinigung übernehmen. Innerhalb des ctorCodes sollten Sie Utils verwenden std::unique_ptr, um die automatische Bereinigung für Sie zu erledigen, wenn etwas ausgelöst wird. Das Ändern von HANDLE h1, h2Feldern in der Klasse zur Unterstützung der automatischen Bereinigung könnte ebenfalls eine gute Idee sein.
Quetzalcoatl
Dies bedeutet, dass der Ctor aussehen sollte: MyClass(){ cleanupGuard1<HANDLE> tmp_h1(&SomeAPIToDestroyA) = SomeAPIToCreateA(); cleanupGuard2<HANDLE> tmp_h2(&SomeAPIToDestroyB) = SomeAPIToCreateB(); if(error) { throw MyException(); } this->h1 = tmp_h1.release(); this->h2 = tmp_h2.release(); }und das ist es . Keine riskante manuelle Bereinigung, keine Aufbewahrungsgriffe in teilweise konstruierten Objekten, bis alles sicher ist, ist ein Bonus. Wenn Sie HANDLE h1,h2in der Klasse zu cleanupGuard<HANDLE> h1;etc wechseln , brauchen Sie das möglicherweise gar nicht dtor.
Quetzalcoatl
Die Implementierung von cleanupGuard1und cleanupGuard2hängt davon ab, was eine relevante xxxToCreateRückgabe bewirkt und welche Parameter eine relevante Rückgabe xxxxToDestroyübernimmt. Wenn sie einfach sind, müssen Sie möglicherweise gar nichts schreiben, da sich oft herausstellt, dass std::unique_ptr<x,deleter()>(oder eine ähnliche) in beiden Fällen den Trick für Sie tun kann.
Quetzalcoatl
-2

Es wurde ein weiteres Beispiel gefunden, in dem Sie Destruktoren manuell aufrufen müssten. Angenommen, Sie haben eine variantenähnliche Klasse implementiert, die einen von mehreren Datentypen enthält:

struct Variant {
    union {
        std::string str;
        int num;
        bool b;
    };
    enum Type { Str, Int, Bool } type;
};

Wenn die VariantInstanz a enthielt std::stringund Sie der Union jetzt einen anderen Typ zuweisen, müssen Sie den std::stringersten zerstören . Der Compiler macht das nicht automatisch .

Violette Giraffe
quelle
-4

Ich habe eine andere Situation, in der ich es für absolut vernünftig halte, den Destruktor anzurufen.

Wenn Sie eine Methode vom Typ "Zurücksetzen" schreiben, um ein Objekt in seinen Ausgangszustand zurückzusetzen, ist es durchaus sinnvoll, den Destruktor aufzurufen, um die alten Daten zu löschen, die zurückgesetzt werden.

class Widget
{
private: 
    char* pDataText { NULL  }; 
    int   idNumber  { 0     };

public:
    void Setup() { pDataText = new char[100]; }
    ~Widget()    { delete pDataText;          }

    void Reset()
    {
        Widget blankWidget;
        this->~Widget();     // Manually delete the current object using the dtor
        *this = blankObject; // Copy a blank object to the this-object.
    }
};
abelenky
quelle
1
Würde es nicht sauberer aussehen, wenn Sie eine spezielle cleanup()Methode deklarieren würden , die in diesem Fall und im Destruktor aufgerufen werden soll ?
Violette Giraffe
Eine "spezielle" Methode, die nur in zwei Fällen aufgerufen wird? Klar ... das klingt völlig richtig (/ Sarkasmus). Methoden sollten verallgemeinert sein und überall aufgerufen werden können. Wenn Sie ein Objekt löschen möchten, ist es nichts Falsches, seinen Destruktor aufzurufen.
Abelenky
4
Sie dürfen den Destruktor in dieser Situation nicht explizit aufrufen. Sie müssten sowieso einen Zuweisungsoperator implementieren.
Rémi