Ist Destruktor manuell immer ein Zeichen für schlechtes Design?

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

Der Destruktor muss manuell std::nothrow werden, wenn das Objekt mit einer überladenen Form des operator new() , außer wenn die Überladungen ” std::nothrow ” verwendet werden:

 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); 

Das externe Verwalten von Speicher auf einer ziemlich niedrigen Ebene, wie oben explizit beschrieben, ist jedoch ein Zeichen für schlechtes Design. Wahrscheinlich ist es eigentlich nicht nur schlechtes Design, sondern geradezu falsch (ja, die Verwendung eines expliziten Destruktors gefolgt von einem Kopierkonstruktoraufruf im Zuweisungsoperator ist ein schlechtes Design und wahrscheinlich falsch).

Mit C ++ 2011 gibt es einen weiteren Grund, explizite Destruktoraufrufe zu verwenden: Wenn generalisierte Vereinigungen verwendet werden, ist es erforderlich, das aktuelle Objekt explizit zu zerstören und ein neues Objekt zu erstellen, indem bei der Änderung des Typs des dargestellten Objekts die neue Position verwendet wird. Wenn die Vereinigung zerstört wird, ist es außerdem erforderlich, den Destruktor des aktuellen Objekts explizit aufzurufen, wenn es zerstört werden muss.

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

Sie rufen das dtor immer dann explizit auf, wenn Sie das Objekt (im Sinne von C ++) einfach zerstören müssen, ohne den Speicher freizugeben, in dem sich das Objekt befindet.

Dies geschieht typischerweise in der gesamten Situation, in der die Speicherzuweisung / -freigabe unabhängig von der Objektkonstruktion / -zerstörung verwaltet wird. In diesen Fällen erfolgt die Konstruktion über eine neue Platzierung auf einem vorhandenen Speicherstück, und die Zerstörung erfolgt über einen 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 anderes bemerkenswertes Beispiel ist der Standard- std::allocator wenn er von std::vector : Elemente werden während push_back in vector push_back , aber der Speicher wird in Chunks zugewiesen, so dass er die Element-Konstruktion push_back . Und daher muss vector::erase die Elemente zerstören, aber nicht unbedingt die Speicherfreigabe (besonders wenn bald neue Pushbacks passieren müssen …).

Es ist “schlechtes Design” im strengen OOP-Sinne (Sie sollten Objekte verwalten, nicht Speicher: die Tatsache, dass Objekte Speicher erfordern, ist ein “Vorfall”), es ist “gutes Design” in “Low-Level-Programmierung” oder in Fällen, in denen Speicher ist nicht aus dem “free store”, den der Standardbetreiber operator new einkauft.

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

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.

Um ein Objekt dynamisch zu erstellen, T* t = new T; unter der Haube: 1. sizeof (T) Speicher ist zugeordnet. 2. Der Konstruktor von T wird aufgerufen, um den zugewiesenen Speicher zu initialisieren. Der Operator new hat zwei Dinge: Zuweisung und Initialisierung.

Um das Objekt zu delete t; unter der Haube: 1. T Destructor wird aufgerufen. 2. Der für das Objekt zugewiesene Speicher wird freigegeben. Der Operator löscht auch zwei Dinge: Zerstörung und Freigabe.

Man schreibt den Konstruktor für die Initialisierung und den Destruktor für die Zerstörung. Wenn Sie den Destruktor explizit aufrufen, wird nur die Destruktion durchgeführt, nicht jedoch die Freigabe .

Eine legitime Verwendung von explizit aufrufendem Destruktor könnte daher lauten: “Ich möchte nur das Objekt zerstören, aber ich kann (noch) nicht die Speicherzuweisung freigeben (oder kann nicht).”

Ein typisches Beispiel hierfür ist die Vorbelegung von Speicher für einen Pool bestimmter Objekte, die ansonsten dynamisch zugeordnet werden müssen.

Wenn Sie ein neues Objekt erstellen, erhalten Sie den Speicherbereich aus dem zuvor zugewiesenen Pool und führen eine “Platzierung neu” aus. Nachdem Sie mit dem Objekt fertig sind, können Sie den Destruktor explizit aufrufen, um die Bereinigungsarbeit abzuschließen, falls vorhanden. Aber Sie werden den Speicher nicht tatsächlich freigeben, wie der Operator löschen würde. Stattdessen geben Sie den Chunk zur Wiederverwendung an den Pool zurück.

Wie in den FAQ angegeben, sollten Sie den Destruktor explizit aufrufen, wenn Sie “placement new” verwenden .

Dies ist das einzige Mal, dass Sie einen Destruktor explizit aufrufen.

Ich stimme jedoch zu, dass dies selten benötigt wird.

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

Z.B.

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

Wenn Sie die gleichen Operationen ausführen müssen, sollten Sie eine separate Methode verwenden.

Es gibt eine spezielle Situation, in der Sie möglicherweise einen Destruktor für ein dynamisch zugewiesenes Objekt mit einer new Position aufrufen möchten, aber es klingt nicht so, als würden Sie jemals etwas brauchen.

Jedes Mal, wenn Sie die Zuweisung von der Initialisierung trennen müssen, benötigen Sie einen neuen und expliziten Aufruf des Destruktors manuell. Heute ist es selten notwendig, da wir die Standardcontainer haben, aber wenn Sie eine neue Art von Container implementieren müssen, werden Sie es brauchen.

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

In Code, an dem ich arbeite, benutze ich explizite Destruktoraufrufzuweisungen, ich habe die Implementierung eines einfachen Zuordners, der die Platzierung neu verwendet, um Speicherblöcke in STL-Container zurückzugeben. In zerstören Ich habe:

  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); } 

Es gibt auch eine Zuweisung in deallocate () in allocate () und Speicherdeallokation unter Verwendung plattformspezifischer Alloc- und Dealloc-Mechanismen. Dieser Zuordner wurde verwendet, um doug lea malloc zu umgehen und direkt LocalAlloc für Windows zu verwenden.

Was ist damit?
Destruktor wird nicht aufgerufen, wenn eine Ausnahme vom Konstruktor ausgetriggers wird. Daher muss ich sie manuell aufrufen, um Handles zu löschen, 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); } }; 

Ich bin nie auf eine Situation gestoßen, in der man einen Destruktor manuell aufrufen muss. Ich glaube mich zu erinnern, dass selbst Stroustrup behauptet, es sei eine schlechte Übung.

Ich fand 3 Gelegenheiten, wo ich das tun musste:

  • Zuweisen / Freigeben von Objekten in Speicher, der von Speicherabbildungs- oder gemeinsam genutztem Speicher erstellt wurde
  • bei der Implementierung einer gegebenen C-Schnittstelle mit C ++ (ja, das passiert heute leider immer noch leider (weil ich nicht genug Einfluss habe, um es zu ändern))
  • bei der Implementierung von Zuweisungsklassen

Ein anderes Beispiel gefunden, in dem Sie Destruktoren manuell aufrufen müssen. Angenommen, Sie haben eine variantenähnliche class implementiert, die eine von mehreren Arten von Daten enthält:

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

Wenn die Variant Instanz eine std::string und Sie nun der Union einen anderen Typ zuweisen, müssen Sie zuerst die std::string . Der Compiler wird das nicht automatisch tun .

Speicher ist nicht anders als andere Ressource: Sie sollten sich http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style ansehen, insbesondere den Teil, wo Bjarne über RAII spricht ( ca. ~ 30min)

Alle notwendigen Vorlagen (shared_ptr, unique_ptr, weak_ptr) sind Teil der C ++ 11 Standardbibliothek

Ich habe eine andere Situation, in der ich denke, dass es vollkommen vernünftig ist, den Destruktor zu nennen.

Wenn Sie eine Methode vom Typ “Zurücksetzen” schreiben, um ein Objekt in seinen ursprünglichen Zustand zurückzuversetzen, ist es 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. } };