Destruktor wird aufgerufen, nachdem er von einem Konstruktor geworfen wurde

73

Früher dachte ich, wenn in C ++ ein Konstruktor eine Ausnahme auslöst, wird der Destruktor dieser "teilweise konstruierten" Klasse nicht aufgerufen.

Aber es scheint, dass es in C ++ 11 nicht mehr stimmt: Ich habe den folgenden Code mit g ++ kompiliert und er druckt " X destructor" auf die Konsole. Warum ist das?

#include <exception>
#include <iostream>
#include <stdexcept>
using namespace std;

class X
{
public:
    X() : X(10)
    {
        throw runtime_error("Exception thrown in X::X()");    
    }
    X(int a)
    {
        cout << "X::X(" << a << ")" << endl;
    }
    ~X()
    {
        cout << "X destructor" << endl;
    }
};

int main()
{
    try
    {
        X x;
    }
    catch(const exception& e)
    {
        cerr << "*** ERROR: " << e.what() << endl;
    }
}

Ausgabe

Standard out:
X::X(10) 
X destructor
Standard error: 
*** ERROR: Exception thrown in X::X()
Mr.C64
quelle
Hier ist ein Liveworkspace-Link , der die Ausgabe zeigt
Sam Miller

Antworten:

81

Das Delegieren von Konstruktoren ist in der Tat eine neue Funktion, die eine neue Zerstörungslogik einführt.

Lassen Sie uns die Lebensdauer eines Objekts erneut betrachten: Die Lebensdauer eines Objekts beginnt, wenn ein Konstruktor fertig ist. (Siehe 15.2 / 2. Der Standard nennt dies den "Hauptkonstruktor".) In Ihrem Fall ist dies der Konstruktor X(int). Der zweite delegierende Konstruktor X()fungiert jetzt nur noch als einfache Elementfunktion. Beim Abwickeln des Bereichs werden die Destruktoren aller vollständig konstruierten Objekte aufgerufen, einschließlich x.

Die Auswirkungen davon sind tatsächlich ziemlich tiefgreifend: Sie können jetzt "komplexe" Arbeitslasten in einen Konstruktor einfügen und die übliche Ausnahmeverbreitung voll ausnutzen, solange Sie Ihren Konstruktor an einen anderen Konstruktor delegieren. Ein solches Design kann die Notwendigkeit verschiedener "init" -Funktionen überflüssig machen, die früher immer dann beliebt waren, wenn nicht gewünscht wurde, zu viel Arbeit in einen regulären Konstruktor zu stecken.

Die spezifische Sprache, die das angezeigte Verhalten definiert, ist:

[C++11: 15.2/2]: [..] Wenn der nicht delegierende Konstruktor für ein Objekt die Ausführung abgeschlossen hat und ein delegierender Konstruktor für dieses Objekt mit einer Ausnahme beendet wird, wird der Destruktor des Objekts aufgerufen. [..]

Kerrek SB
quelle
8
Verwendung[C++11: 15.2/2]
Leichtigkeitsrennen im Orbit
2
@Nawaz: "Grundlegende Konzepte - Objektlebensdauer"?
Kerrek SB
@KerrekSB: Das Zitat aus der gelöschten Antwort von Lightness_Races_in_Orbit wurde hinzugefügt. ;-)
Nawaz
26

Früher dachte ich, wenn in C ++ ein Konstruktor eine Ausnahme auslöst, wird der Destruktor dieser "teilweise konstruierten" Klasse nicht aufgerufen.

Aber es scheint, dass es in C ++ 11 nicht mehr stimmt

Es ist immer noch wahr. Seit C ++ 03 hat sich nichts geändert (für einen Wert von nichts ;-))

Was Sie dachten, ist immer noch wahr, aber es gibt kein teilweise konstruiertes Objekt, wenn die Ausnahme ausgelöst wird.

Der C ++ 03 TC1-Standard sagt (Hervorhebung von mir):

Bei einem Objekt, das teilweise erstellt oder teilweise zerstört wurde, werden Destruktoren für alle vollständig konstruierten Unterobjekte ausgeführt, dh für Unterobjekte, für die der Konstruktor die Ausführung abgeschlossen hat und der Destruktor noch nicht mit der Ausführung begonnen hat.

dh Jedes Objekt, das seinen Konstruktor fertiggestellt hat, wird durch Ausführen des Destruktors zerstört. Das ist eine schöne einfache Regel.

Grundsätzlich gilt in C ++ 11 dieselbe Regel: Sobald X(int)der Objektkonstruktor die Ausführung abgeschlossen hat, ist er vollständig konstruiert, sodass er vollständig konstruiert ist und sein Destruktor zum richtigen Zeitpunkt ausgeführt wird (wenn er den Gültigkeitsbereich verlässt oder ein Die Ausnahme wird in einem späteren Stadium des Aufbaus ausgelöst.) Es ist im Wesentlichen immer noch dieselbe Regel.

Der Hauptteil des delegierenden Konstruktors läuft dem anderen Konstruktor nach und kann zusätzliche Arbeit leisten. Dies ändert jedoch nichts an der Tatsache, dass die Konstruktion des Objekts abgeschlossen ist, sodass es vollständig erstellt wird. Der delegierende Konstruktor ist analog zu einem Konstruktor einer abgeleiteten Klasse, der nach Abschluss eines Konstruktors einer Basisklasse mehr Code ausführt. In gewissem Sinne können Sie Ihr Beispiel folgendermaßen betrachten:

class X
{
public:
    X(int a)
    {
        cout << "X::X(" << a << ")" << endl;
    }
    ~X()
    {
        cout << "X destructor" << endl;
    }
};
    
class X_delegating : X
{
public:
    X_delegating() : X(10)
    {
        throw runtime_error("Exception thrown in X::X()");    
    }
};

Es ist nicht wirklich so, es gibt nur einen Typ, aber es ist insofern analog, als der X(int)Konstruktor ausgeführt wird, dann wird zusätzlicher Code im delegierenden Konstruktor ausgeführt, und wenn dies die X"Basisklasse" auslöst (die eigentlich keine Basisklasse ist) wird zerstört.

Jonathan Wakely
quelle
3
Seltsamerweise bekommt diese Antwort nur eine Gegenstimme. Obwohl Kerrek erwähnt, warum diese Funktion vorhanden ist, kann diese meiner Meinung nach die eigentliche Frage besser beantworten.
@Tibo, ja, ich denke auch ;-) Ich war zu spät zur Party, ich glaube, Kerreks Antwort wurde bereits akzeptiert, als ich meine schrieb.
Jonathan Wakely