Ist es niemals sicher, eine Ausnahme in einem Konstruktor zu werfen?

8

Ich weiß, dass es nicht sicher ist, Ausnahmen von Destruktoren zu werfen, aber ist es immer gefährlich, Ausnahmen von Konstruktoren zu werfen?

z.B. Was passiert bei global deklarierten Objekten? Ein schneller Test mit gcc und ich bekomme ein Abbruch, ist das immer garantiert? Welche Lösung würden Sie verwenden, um dieser Situation gerecht zu werden?

Gibt es Situationen, in denen Konstruktoren Ausnahmen auslösen können und die Dinge nicht so lassen, wie wir es erwarten?

EDIT: Ich sollte hinzufügen, dass ich versuche zu verstehen, unter welchen Umständen ich ein Ressourcenleck bekommen könnte. Sieht so aus, als wäre es sinnvoll, die Ressourcen, die wir während der Bauphase erhalten haben, manuell freizugeben, bevor wir die Ausnahme auslösen. Ich musste nie zuvor Ausnahmen in Konstruktoren werfen, um zu verstehen, ob es irgendwelche Fallstricke gibt.

d. Ist das auch sicher?

%Vor%

Wird der dem Objekt P zugewiesene Speicher freigegeben?

EDIT2: Vergessen Sie zu erwähnen, dass in diesem speziellen Fall dostuff den Verweis auf P in einer Ausgabewarteschlange speichert. P ist eigentlich eine Nachricht und dostuff übernimmt die Nachricht, leitet sie an die entsprechende Ausgabewarteschlange weiter und sendet sie. Im Wesentlichen, sobald Dostuff es hat, wird es später in den Innereien von Dostuff freigesetzt. Ich denke, ich möchte ein Autoptr um P setzen und Release auf dem Autoptrt nach Dostuff aufrufen, um ein Speicherleck zu verhindern, wäre das korrekt?

    
Matt 29.07.2009, 00:58
quelle

5 Antworten

28

Das Auslösen von Ausnahmen von einem Konstruktor ist eine gute Sache . Wenn etwas in einem Konstruktor fehlschlägt, haben Sie zwei Möglichkeiten:

  • Pflegen Sie einen "Zombie" -Zustand, in dem die Klasse existiert, aber nichts tut, oder
  • Eine Ausnahme auslösen.

Und die Aufrechterhaltung von Zombie-Klassen kann ein ziemlicher Ärger sein, wenn die wirkliche Antwort gewesen sein sollte, "das ist fehlgeschlagen, was nun?".

Gemäß dem Standard in 3.6.2.4:

  

Wenn die Konstruktion oder die Zerstörung eines nicht-lokalen statischen Objekts mit dem Übergeben einer nicht abgefangenen Ausnahme endet, muss das Ergebnis terminate (18.6.3.3) aufgerufen werden.

Wo terminate sich auf std::terminate bezieht.

Was Ihr Beispiel betrifft, nein. Dies liegt daran, dass Sie keine RAII-Konzepte verwenden. Wenn eine Ausnahme ausgelöst wird, wird der Stapel abgewickelt, was bedeutet, dass alle Objekte ihre Destruktoren aufgerufen bekommen, wenn der Code die nächste entsprechende catch -Klausel erreicht.

Ein Zeiger hat keinen Destruktor. Machen wir einen einfachen Testfall:

%Vor%

Hier wird str Speicher zuweisen und "Blah" kopieren. hinein und pi wird initialisiert, um auf eine Ganzzahl im Speicher zu zeigen.

Wenn eine Ausnahme ausgelöst wird, beginnt das Abwickeln des Stapels. Es wird zuerst den Destruktor des Zeigers "aufrufen" (nichts tun), dann den Destruktor str , der den ihm zugewiesenen Speicher freigibt.

Wenn Sie RAII-Konzepte verwenden, verwenden Sie einen intelligenten Zeiger:

%Vor%

Hier wird% destructor pi delete aufrufen und kein Speicher wird geleakt. Aus diesem Grund sollten Sie Ihre Zeiger immer umbrechen, und das ist der gleiche Grund, warum wir std::vector verwenden, anstatt manuell Zeiger zuzuweisen, ihre Größe zu ändern und sie zu löschen. (Sauberkeit und Sicherheit)

Bearbeiten

Ich habe vergessen zu erwähnen. Du hast das gefragt:

  

Ich denke, ich möchte ein Autoptr um P setzen und die Freigabe auf dem Autoptrt nach Dostuff aufrufen, um ein Speicherleck zu verhindern, wäre das korrekt?

Ich habe es nicht explizit erwähnt und oben nur angedeutet, aber die Antwort ist nein . Alles, was Sie tun müssen, ist es in auto_ptr zu platzieren und wenn es soweit ist, wird es automatisch gelöscht. Wenn Sie es manuell freigeben, wird der Zweck, es in einem Container zu platzieren, von vornherein aufgehoben.

Ich würde auch vorschlagen, dass Sie sich fortgeschrittene Smart Pointer ansehen, wie zum Beispiel die in Boost . Ein außerordentlich populärer ist shared_ptr , was reference gezählt , so dass es für die Lagerung in Containern geeignet ist und kopiert werden kann. (Im Gegensatz zu auto_ptr . Verwenden Sie nicht auto_ptr in Containern!)

    
GManNickG 29.07.2009, 01:05
quelle
8

Als Spence erwähnt , das Auswerfen eines Konstruktors (oder das Zulassen, dass eine Exception einem Konstruktor entgehen kann) birgt das Risiko von Ressourcenverlusten, wenn der Konstruktor nicht sorgfältig dafür geschrieben wird, diesen Fall zu behandeln.

Dies ist ein wichtiger Grund, warum RAII-Objekte (wie Smartpointer) bevorzugt werden sollten - sie werden automatisch die Bereinigung bei Ausnahmen durchführen.

Wenn Sie Ressourcen haben, die gelöscht oder auf andere Weise manuell freigegeben werden müssen, müssen Sie sicherstellen, dass sie bereinigt werden, bevor die Ausnahme abbricht. Das ist nicht immer so einfach wie es klingen mag (und sicherlich nicht so einfach, wie ein RAII-Objekt es automatisch verarbeiten lässt).

Und vergessen Sie nicht, wenn Sie die Bereinigung für etwas, das in der Initialisierungsliste des Konstruktors passiert, manuell behandeln müssen, müssen Sie die funky Funktion "try-try-block" verwenden:

%Vor%

Denken Sie auch daran, dass die Ausnahmesicherheit der Hauptgrund dafür ist, dass das Idiom "Austauschen" weit verbreitet ist - es ist ein einfacher Weg, um sicherzustellen, dass Kopierkonstrukte keine Objekte auslecken oder beschädigen.

Also, die Quintessenz ist, dass die Verwendung von Ausnahmen zur Behandlung von Fehlern in Konstruktoren in Ordnung ist, aber es ist nicht unbedingt automatisch.

    
Michael Burr 29.07.2009 02:14
quelle
3

Die vorherigen Antworten waren ausgezeichnet. Ich möchte nur eine Sache hinzufügen, basierend auf Martin Yorks und Michael Burrs Antworten.

Mit dem Beispielkonstruktor von Michael Burr habe ich im Konstruktorkörper eine Zuweisung hinzugefügt:

%Vor%

Die Frage ist jetzt, wann wird d als 'vollständig konstruiert' betrachtet , so dass ihr Destruktor aufgerufen wird, wenn eine Exception innerhalb des Konstruktors geworfen wird (wie in Martins Post)? Die Antwort lautet: nach dem Initialisierer

%Vor%

Der Punkt ist, dass die Felder Ihres Objekts immer ihre Destruktoren aufrufen, wenn eine Ausnahme vom Konstruktor body ausgelöst wird. (Dies gilt unabhängig davon, ob Sie tatsächlich Initialisierer für sie angegeben haben.) Umgekehrt wird, wenn eine Ausnahme aus dem Initialisierer eines anderen Feldes ausgelöst wird, Destruktoren für die Felder aufgerufen, deren Initialisierer bereits ausgeführt wurden ( und nur für diese Felder.)

Dies bedeutet, dass die beste Vorgehensweise darin besteht, keinem Feld zu erlauben, den Konstruktorkörper mit einem unzerstörbaren Wert zu erreichen (z. B. ein undefinierter Zeiger). In diesem Fall ist es am besten, Ihren Feldern durch die Initialisierer ihre tatsächlichen Werte zu geben , anstatt (sagen wir) zuerst die Zeiger auf NULL zu setzen und ihnen dann ihre 'echten' Werte im Konstruktorkörper zu geben.

    
Dan Breslau 29.07.2009 03:37
quelle
3

Wenn Sie eine Ausnahme von einem Konstruktor werfen, werden einige Dinge passieren.

1) Alle vollständig konstruierten Mitglieder werden ihre Destruktoren haben.
2) Der für das Objekt zugewiesene Speicher wird freigegeben.

Um Dinge automatisch zu machen, sollten Sie keinen RAW-Zeiger in Ihrer Klasse haben, einer der Standard-Smart-Pointer wird normalerweise den Trick machen und die Compiler-Optimierung wird den meisten Overhead auf praktisch nichts reduzieren [oder nur die Arbeit, die Sie haben sollten manuell sowieso gemacht].

Übergabe von Zeigern an Funktionen

Die andere Sache, die ich NICHT machen würde; Übergibt einen Wert an eine Funktion als Zeiger.
Das Problem ist, dass Sie nicht angeben, wem das Objekt gehört. Ohne die impliziten Eigentumsinformationen ist es unklar (wie bei allen C-Funktionen), wer für die Bereinigung des Zeigers verantwortlich ist.

%Vor%

Sie erwähnen, dass p in einem que gespeichert und zu einem späteren Zeitpunkt verwendet wird. Dies bedeutet, dass Sie das Objekt an die Funktion übergeben. Machen Sie diese Beziehung explizit, indem Sie std :: auto_ptr verwenden. Auf diese Weise weiß der Aufrufer von dostuff (), dass er den Zeiger nach dem Aufruf von dostuff () nicht benutzen kann, da der Aufruf der Funktion den Zeiger tatsächlich in die Funktion übertragen hat (dh der Aufrufer local auto_ptr wird danach einen NULL-Zeiger enthalten) der Anruf zu dostuff ()).

%Vor%

Speichern von Zeigern in einem Cointainer

Sie erwähnen, dass dostuff () verwendet wird, um eine Liste von p-Objekten für die verzögerte Verarbeitung zu speichern.
Das bedeutet, dass Sie die Objekte in einen Container stecken. Jetzt unterstützen die normalen Container nicht std :: auto_ptr. Boost unterstützt jedoch Container mit Zeigern (wo der Container Besitz übernimmt). Auch diese contairs verstehen auto_ptr und übertragen automatisch den Besitz von der auto_ptr auf den Container.

%Vor%

Hinweis: Wenn Sie auf Member dieser Container zugreifen, wird immer ein Verweis (kein Zeiger) auf das enthaltene Objekt zurückgegeben. Dies zeigt an, dass die Lebensdauer des Objekts an die Lebensdauer des Containers gebunden ist und die Referenz gültig ist, solange der Container gültig ist (es sei denn, Sie entfernen das Objekt explizit).

    
Martin York 29.07.2009 02:40
quelle
0

Wenn Sie eine Ressource in einem Konstruktor verwenden, z. B. einen Socket usw., dann würde dies geleakt werden, wenn Sie eine Ausnahme auslösen würden, oder?

Aber ich denke, das ist das Argument dafür, in einem Konstruktor keine Arbeit zu verrichten, indem Sie Ihre Verbindungen so initialisieren, wie Sie sie brauchen.

    
Spence 29.07.2009 01:02
quelle

Tags und Links