it-swarm.com.de

Ist der Aufruf des Destruktors manuell immer ein Zeichen von schlechtem Design?

Ich habe nachgedacht: Sie sagen, wenn Sie Destruktor manuell aufrufen - Sie machen etwas falsch. Aber ist es immer so? Gibt es Gegenbeispiele? Situationen, in denen ein manuelles Anrufen erforderlich ist oder in denen es schwierig/unmöglich/unpraktisch ist, es zu vermeiden?

71
Violet Giraffe

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

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 der Verwaltung von Arbeitsspeicher auf einer eher niedrigen Ebene als oben explizit aufrufende Destruktoren, ist jedoch ein Zeichen schlechten Designs. Wahrscheinlich ist es nicht nur ein schlechtes Design, sondern geradezu falsch (ja, die Verwendung eines expliziten Destruktors gefolgt von einem Aufruf des Copy-Konstruktors im Zuweisungsoperator ist ein schlechtes Design und wahrscheinlich falsch).

Bei C++ 2011 gibt es einen weiteren Grund, explizite Destruktoraufrufe zu verwenden: Wenn Sie generalisierte Unionen verwenden, müssen Sie das aktuelle Objekt explizit löschen und ein neues Objekt erstellen, indem Sie den Typ neu setzen, wenn Sie den Typ des dargestellten Objekts ändern. Wenn die Vereinigung zerstört wird, muss der Destruktor des aktuellen Objekts explizit aufgerufen werden, wenn eine Zerstörung erforderlich ist.

80
Dietmar Kühl

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

Sie rufen den dtor jedes Mal explizit auf, wenn Sie einfach das object (im Sinne von C++) zerstören müssen, ohne den memory das Objekt zu verlassen.

Dies tritt normalerweise in allen Situationen auf, in denen die Speicherzuweisungfreigabe unabhängig von der Objektkonstruktion/-vernichtung verwaltet wird. In diesen Fällen erfolgt die Konstruktion über/- Platzieren neu auf einem vorhandenen Speicherblock, und die Zerstörung erfolgt durch expliziten Aufruf von dtor.

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 Standardwert std::allocator, wenn er von std::vector verwendet wird: Elemente werden während Push_back in vector erstellt, der Speicher ist jedoch in Blöcken zugeordnet, sodass die Elementstruktur bereits vorhanden ist. Daher muss vector::erase die Elemente zerstören, aber es muss nicht unbedingt den Speicher aufheben (insbesondere, wenn in Kürze ein neuer Push_back ausgeführt werden muss ...).

Es ist "schlechtes Design" im strengen OOP Sinn (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 nicht aus dem "freien Speicher" genommen wird, kauft der Standardcode operator new.

Es ist ein schlechtes Design, wenn es zufällig um den Code herum geschieht. Es ist ein gutes Design, wenn es lokal für speziell für diesen Zweck entwickelte Klassen geschieht.

86

Nein, hängt von der Situation ab, manchmal ist sie legitim und good design.

Um zu verstehen, warum und wann Sie Destruktoren explizit aufrufen müssen, schauen wir uns an, was mit "new" und "delete" passiert.

Um ein Objekt dynamisch zu erstellen, T* t = new T; unter der Haube: 1. Speichergröße (T) wird zugewiesen. 2. Der Konstruktor von T wird aufgerufen, um den zugewiesenen Speicher zu initialisieren. Der Operator new führt zwei Dinge aus: Zuordnung und Initialisierung.

Um das Objekt delete t; unter der Haube zu zerstören: 1. Der Destruktor von T wird aufgerufen. 2. Der für dieses Objekt zugewiesene Speicher wird freigegeben. Der Operator delete führt auch zwei Dinge aus: 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 Zerstörung ausgeführt, aber nicht die Freigabe.

Eine legitime Verwendung des explizit aufrufenden Destruktors könnte daher lauten: "Ich möchte nur das Objekt zerstören, aber ich kann (oder kann) die Speicherzuweisung (noch) nicht freigeben."

Ein allgemeines Beispiel hierfür ist die Vorabzuordnung von Speicher für einen Pool bestimmter Objekte, die andernfalls dynamisch zugewiesen werden müssen. 

Wenn Sie ein neues Objekt erstellen, erhalten Sie den Speicherplatz aus dem vorab zugewiesenen Pool und führen eine "Platzierung neu" durch. Wenn Sie mit dem Objekt fertig sind, möchten Sie möglicherweise den Destruktor explizit aufrufen, um die Bereinigungsarbeiten abzuschließen, sofern vorhanden. Sie werden jedoch den Speicher nicht wirklich freigeben, wie es der Operator delete getan hätte. Stattdessen geben Sie den Block zur erneuten Verwendung in den Pool zurück. 

8
user1252446

Wie in den FAQ zitiert, sollten Sie den Destruktor explizit aufrufen, wenn Sie placement new verwenden.

Dies ist ungefähr die einzige Zeit, in der Sie einen Destruktor explizit aufrufen.

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

7
Luchian Grigore

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 Operationen ausführen müssen, sollten Sie über eine separate Methode verfügen.

Es gibt eine spezifische Situation , in der Sie einen Destruktor für ein dynamisch zugewiesenes Objekt mit einer Platzierung new aufrufen können, aber es klingt nicht nach etwas, das Sie jemals brauchen werden.

6
Jack

Jedes Mal, wenn Sie die Zuordnung von der Initialisierung trennen müssen, müssen Sie Manuell neu platzieren und den Destruktor explizit aufrufen. Heutzutage ist dies selten notwendig, da wir die Standardcontainer haben. Wenn Sie jedoch eine neue Sortierung Von Containern implementieren müssen, benötigen Sie sie. 

4
James Kanze

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

In Code, an dem ich arbeite, verwende ich expliziten Destruktoraufruf in Allokatoren. Ich habe eine einfache Zuweisung, die Platzierung 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);
  }

es wird auch eine Zuweisung in Allocation () und Speicherfreigabe in Deallocate () vorgenommen, wobei plattformspezifische Allokations- und Dealloc-Mechanismen verwendet werden. Dieser Allokator wurde verwendet, um Doug Lea Malloc zu umgehen und beispielsweise LocalAlloc direkt unter Windows zu verwenden.

2
marcinj

Ich bin nie auf eine Situation gestoßen, in der man einen Destruktor manuell aufrufen muss. Ich glaube, sogar Stroustrup behauptet, es sei eine schlechte Praxis.

1
Lieuwe

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

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

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

  • zuordnen/Aufheben der Zuordnung 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 heute leider immer noch (weil ich nicht genug Einfluss habe, um es zu ändern))
  • bei der Implementierung von Zuweisungsklassen
0
user4590120