2014-06-26 20 views
5

Im folgenden Code wird ein X in einem globalen Container registriert, der zu einem geteilten Besitzer desselben wird. Der Destruktor von X testet, dass er nicht länger Teil eines solchen Besitzes ist, was ich als eine gültige Voraussetzung für die Zerstörung ansehen würde.Sollte der Besitz vor oder nach den STL-Containern beendet werden, rufen Sie dessen Destruktor auf?

#include <vector> 
#include <memory> 

struct X { 
    ~X(); 
}; 

std::vector<std::shared_ptr<X> > global_x_reg; 

X::~X() 
{ 
    for (auto iter = global_x_reg.begin(), end = global_x_reg.end(); iter != end; ++iter) 
     if (iter->get() == this) 
      throw "Oops. X gets destroyed while it is still owned!"; 
} 

int main(int argc, char** argv) 
{ 
    global_x_reg.push_back(std::shared_ptr<X>(new X)); 
    global_x_reg.clear(); // calls X::~X(). 
} 

Wenn es läuft (nach der Kompilierung mit VS2010), "Oops ..." wird ausgelöst, wenn der Behälter gelöscht wird.

Fragen:

  1. ist dieser Code legal? Wenn nicht, warum nicht? Wenn ja, sollte es werfen?
  2. sollte ein Standard-Container clear() so implementieren, dass diese Werte bei der Zerstörung seiner Werte nicht mehr als Containes sichtbar sind.
  3. sollte std::shared_ptr::get, wenn std::shared_ptr seinen Postee zerstört, zurückgeben nullptr?

Antwort

4

Per N3936 [basic.life]/1: "Die Lebensdauer eines Objekts mit nicht-trivialen Initialisierung endet, wenn der Anruf destructor beginnt.", Und/3:

Die Eigenschaften überall auf Objekte zugeschrieben Diese Internationale Norm gilt für ein bestimmtes Objekt nur während seiner Lebensdauer. [] Hinweis: Insbesondere bevor die Lebensdauer eines Objekts beginnt und nachdem seine Lebensdauer endet, gibt es erhebliche Einschränkungen bei der Verwendung des Objekts, wie unten in 12.6.2 und in 12.7 beschrieben. Außerdem ist das Verhalten eines Objekts, das sich in Konstruktion und Zerstörung befindet, möglicherweise nicht das gleiche wie das Verhalten eines Objekts, dessen Lebensdauer begonnen hat und nicht beendet wurde. 12.6.2 und 12.7 beschreiben das Verhalten von Objekten während der Konstruktions- und Zerstörungsphase.- Endnote]

Sie eine Member-Funktion auf einem shared_ptr nach dem Ende seiner Lebensdauer aufgerufen wird. Da es keine Möglichkeit gibt, zu wissen, ob eine gegebene Memberfunktion einer Standardbibliotheksklasse den angegebenen Einschränkungen entspricht, hat das Aufrufen einer Memberfunktion für ein Standardbibliotheksobjekt nach dem Ende seiner Lebensdauer folglich undefiniertes Verhalten, sofern nicht anders angegeben.

Siehe auch Library Working Group issue 2382 "Unclear order of container update versus object destruction on removing an object", die sehr auf die Frage bezieht. Im Allgemeinen ist es keine gute Idee, ein Standardbibliotheksobjekt (global_x_reg) erneut einzugeben, während es gerade einen Funktionsaufruf bearbeitet (global_x_reg.clear() in diesem Fall). Die Klasseninvarianten müssen natürlich vor und nach einem Member-Aufruf bestehen, aber es gibt keine Garantie, dass sich das Objekt in einem gültigen Zustand während ein Anruf befindet.

+0

Ich bin verwirrt. Wie kann ein Destruktor etwas Nützliches tun, wenn die Lebensdauer des Objekts zu Beginn des Aufrufs abgelaufen ist? Wenn Sie Memberfunktionen für ein Objekt, das zerstört wird, nicht aufrufen können, warum können Sie dann auf seine Datenmitglieder zugreifen? – Simple

+1

@Simple Es ist nicht generell verboten, nach dem Ende seiner Lebensdauer auf ein Objekt zuzugreifen, obwohl das, was Sie mit einem solchen Objekt tun dürfen, sehr eingeschränkt ist. (Siehe die Referenzen im Zitat für [basic.life]/3, die ich vollständig in die Antwort eingefügt habe.) – Casey

+0

@Casey: Danke, die LWG-Ausgabe 2382 gibt eine noch bessere Beschreibung der Situation, der ich gegenüberstand. Ich verstehe jetzt, dass Code nicht wieder in Standardcontainer eingeben sollte. Das OP-Beispiel wurde aus einer realen Codesituation mit stark rekursiven Datenstrukturen abgeleitet. Obwohl ich mit dem LWG-Plakat Peter Kasting übereinstimme, dass es am sinnvollsten ist, zu sagen, dass Objekte aus dem Container entfernt werden sollten, bevor sie zerstört werden, werde ich darauf hinweisen, dass dies eine derzeit gültige Voraussetzung ist, und versuchen, den Wiedereintritt zu vermeiden. –

2

Ich glaube nicht, dass es einen bestimmten Grund, dass .get()muss Rückkehr NULL vor dem deleter (und folglich ~X()) von std::shared_ptr<X>::~shared_ptr<X> aufgerufen wird. Die einzige Anforderung ist, dass sie NULL zurückgibt, sobald std:shared_ptr<X>::~shared_ptr<X> abgeschlossen ist.

Und falls std::vector verwendet Platzierung neu in seine Elemente zu konstruieren und zu zerstören (und es wird), gibt es keinen Grund, dass es Muss haben ihre Accessoren aktualisiert, bevor ein platziertes Element zu zerstören.

In der Tat, wenn Sie diesen gemeinsamen altmodischen Muster denken:

T* ptr = new T(); 
delete ptr; 
ptr = NULL; 

Es ist klar, dass ~T() wird aufgerufen, bevor ptr-NULL eingestellt ist; Das ist ziemlich genau das Gleiche, was Sie sehen.

Der folgende Auszug aus libstdC++ v4.6.3 der Analogie unterstützt:

00353  virtual void 
00354  _M_destroy() // nothrow 
00355  { 
00356  _My_alloc_type __a(_M_del); 
00357  this->~_Sp_counted_deleter(); 
00358  __a.deallocate(this, 1); 
00359  } 

(link)

Kurz gesagt, sind Sie ein völlig harmlose Implementierung Detail zu beobachten und zu versuchen, zu behaupten, dass es angegeben Umbrüche Semantik, wenn ich nicht denke, dass es tut.

+0

Ich denke, dass Sie hier die falsche Frage ansprechen: OP fragt nicht, warum der * Zeiger * immer noch das Objekt besitzt - eher, warum der * Container * zum Zeitpunkt des Aufrufs des Konstruktors immer noch den Smart Pointer enthält. Dies kann verwandt sein (in einer naiven Implementierung ist es definitiv eine Konsequenz), aber es ist ausreichend anders. –

+0

@KonradRudolph: Wahr; Ich wollte das gleiche Argument für Vektor zur gleichen Zeit machen. Das jetzt hinzugefügt. Passender Auszug von libstdC++ ankommendes _ [redigieren Sie: nah, zu komplex] _ –

0

Ich denke, du machst hier eine seltsame Sache. Wenn Sie Zugriff auf Ihre Elemente behalten möchten, sollten Sie X::~X() so implementieren, dass das zerstörte Objekt nicht registriert ist, aber den std :: vector selbst nicht löscht. Außerdem wird der gemeinsame Zeiger innerhalb des Aufrufs zu clear() des in Frage stehenden std::vector ungültig gemacht (und X wird zerstört). Daher ist die std::vector in einem fragilen Zustand und ich würde mich auf keinen Fall zu sehr darauf verlassen. Betrachten Sie das Beispiel einer verketteten Liste: Wenn Sie ein Element entfernen, das im Verlauf zerstört wird und der Destruktor über die Liste iteriert, kann es sein, dass die Referenzen auf Knoten nicht in einem konsistenten Zustand sind. Ich würde diese Situation vermeiden.