Ausnahmen aus einem Destruktor werfen


257

Die meisten Leute sagen, wirf niemals eine Ausnahme aus einem Destruktor heraus - dies führt zu undefiniertem Verhalten. Stroustrup weist darauf hin, dass "der Vektordestruktor den Destruktor explizit für jedes Element aufruft. Dies impliziert, dass die Vektorzerstörung fehlschlägt, wenn ein Elementdestruktor ausgelöst wird ... Es gibt wirklich keine gute Möglichkeit, sich vor Ausnahmen zu schützen, die von Destruktoren ausgelöst werden, also die Bibliothek übernimmt keine Garantie, wenn ein Element-Destruktor auslöst "(aus Anhang E3.2) .

Dieser Artikel scheint etwas anderes zu sagen - dass das Werfen von Destruktoren mehr oder weniger in Ordnung ist.

Meine Frage lautet also: Wenn das Werfen von einem Destruktor zu undefiniertem Verhalten führt, wie gehen Sie mit Fehlern um, die während eines Destruktors auftreten?

Wenn während eines Bereinigungsvorgangs ein Fehler auftritt, ignorieren Sie ihn einfach? Wenn es sich um einen Fehler handelt, der möglicherweise im Stapel behandelt werden kann, aber nicht direkt im Destruktor, ist es nicht sinnvoll, eine Ausnahme aus dem Destruktor auszulösen?

Offensichtlich sind solche Fehler selten, aber möglich.


36
"Zwei Ausnahmen gleichzeitig" ist eine Aktienantwort, aber es ist nicht der WIRKLICHE Grund. Der wahre Grund ist, dass eine Ausnahme nur dann ausgelöst werden sollte, wenn die Nachbedingungen einer Funktion nicht erfüllt werden können. Die Nachbedingung eines Destruktors ist, dass das Objekt nicht mehr existiert. Das kann nicht passieren. Jede fehleranfällige End-of-Life-Operation muss daher als separate Methode aufgerufen werden, bevor das Objekt den Gültigkeitsbereich verlässt (sinnvolle Funktionen haben normalerweise ohnehin nur einen Erfolgspfad).
Spraff

29
@spraff: Ist dir bewusst, dass das, was du gesagt hast, "RAII wegwerfen" impliziert?
Kos

16
@spraff: Wenn Sie "eine separate Methode aufrufen müssen, bevor das Objekt den Gültigkeitsbereich verlässt" (wie Sie geschrieben haben), wird RAII tatsächlich weggeworfen! Code, der solche Objekte verwendet, muss sicherstellen, dass eine solche Methode aufgerufen wird, bevor der Destruktor aufgerufen wird. Schließlich hilft diese Idee überhaupt nicht.
Frunsi

8
@Frunsi nein, weil dieses Problem auf die Tatsache zurückzuführen ist, dass der Destruktor versucht, etwas zu tun, das über die bloße Freigabe von Ressourcen hinausgeht. Es ist verlockend zu sagen "Ich möchte immer XYZ machen" und zu denken, dass dies ein Argument dafür ist, solche Logik in den Destruktor zu stecken. Nein, sei nicht faul, schreibe xyz()und halte den Destruktor frei von Nicht-RAII-Logik.
Spraff

6
@Frunsi Zum Beispiel ist es nicht unbedingt in Ordnung, im Destruktor einer Klasse, die eine Transaktion darstellt , etwas in eine Datei zu schreiben . Wenn das Festschreiben fehlgeschlagen ist, ist es zu spät, um es zu verarbeiten, wenn der gesamte Code, der an der Transaktion beteiligt war, den Gültigkeitsbereich überschritten hat. Der Destruktor sollte die Transaktion verwerfen, sofern keine commit()Methode aufgerufen wird.
Nicholas Wilson

Antworten:


198

Es ist gefährlich, eine Ausnahme aus einem Destruktor herauszuwerfen.
Wenn sich bereits eine andere Ausnahme verbreitet, wird die Anwendung beendet.

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

Dies läuft im Wesentlichen auf Folgendes hinaus:

Alles, was gefährlich ist (dh eine Ausnahme auslösen könnte), sollte über öffentliche Methoden erfolgen (nicht unbedingt direkt). Der Benutzer Ihrer Klasse kann dann möglicherweise mit diesen Situationen umgehen, indem er die öffentlichen Methoden verwendet und mögliche Ausnahmen abfängt.

Der Destruktor beendet das Objekt dann durch Aufrufen dieser Methoden (falls der Benutzer dies nicht explizit getan hat), aber alle Ausnahmen, die ausgelöst werden, werden abgefangen und gelöscht (nachdem versucht wurde, das Problem zu beheben).

Tatsächlich übertragen Sie die Verantwortung auf den Benutzer. Wenn der Benutzer in der Lage ist, Ausnahmen zu korrigieren, ruft er die entsprechenden Funktionen manuell auf und verarbeitet alle Fehler. Wenn der Benutzer des Objekts nicht besorgt ist (da das Objekt zerstört wird), muss sich der Destruktor um das Geschäft kümmern.

Ein Beispiel:

std :: fstream

Die Methode close () kann möglicherweise eine Ausnahme auslösen. Der Destruktor ruft close () auf, wenn die Datei geöffnet wurde, stellt jedoch sicher, dass keine Ausnahmen aus dem Destruktor übertragen werden.

Wenn der Benutzer eines Dateiobjekts eine spezielle Behandlung für Probleme beim Schließen der Datei durchführen möchte, ruft er close () manuell auf und behandelt alle Ausnahmen. Wenn es ihnen andererseits egal ist, bleibt der Destruktor übrig, um die Situation zu bewältigen.

Scott Myers hat einen ausgezeichneten Artikel zu diesem Thema in seinem Buch "Effective C ++".

Bearbeiten:

Anscheinend auch in "Effective C ++"
Punkt 11: Verhindern Sie, dass Ausnahmen Destruktoren verlassen


5
"Wenn es Ihnen nichts ausmacht, die Anwendung möglicherweise zu beenden, sollten Sie den Fehler wahrscheinlich verschlucken." - Dies sollte wahrscheinlich eher die Ausnahme sein (entschuldigen Sie das Wortspiel) als die Regel - das heißt, schnell zu scheitern.
Erik Forbes

15
Ich bin nicht einverstanden. Durch Beenden des Programms wird der Stapel abgewickelt. Es wird kein Destruktor mehr aufgerufen. Alle geöffneten Ressourcen bleiben offen. Ich denke, die Ausnahme zu schlucken wäre die bevorzugte Option.
Martin York

20
Das Betriebssystem kann Ressourcen bereinigen, für die der Eigentümer nicht zuständig ist. Speicher, FileHandles usw. Was ist mit komplexen Ressourcen: DB-Verbindungen. Dieser Uplink zu der ISS, die Sie geöffnet haben (werden automatisch die engen Verbindungen gesendet)? Ich bin sicher, die NASA möchte, dass Sie die Verbindung sauber schließen!
Martin York

7
Wenn eine Anwendung durch Abbruch "schnell fehlschlägt", sollte sie überhaupt keine Ausnahmen auslösen. Wenn dies fehlschlägt, indem die Kontrolle über den Stapel übertragen wird, sollte dies nicht so erfolgen, dass das Programm abgebrochen wird. Der eine oder andere, wählen Sie nicht beide.
Tom

2
@LokiAstari Das Transportprotokoll, mit dem Sie mit einem Raumschiff kommunizieren, kann eine unterbrochene Verbindung nicht verarbeiten? Ok ...
Doug65536

54

Das Herauswerfen eines Destruktors kann zu einem Absturz führen, da dieser Destruktor möglicherweise als Teil des "Abwickelns des Stapels" aufgerufen wird. Das Abwickeln des Stapels ist eine Prozedur, die ausgeführt wird, wenn eine Ausnahme ausgelöst wird. In dieser Prozedur werden alle Objekte beendet, die seit dem "Versuch" und bis zum Auslösen der Ausnahme in den Stapel geschoben wurden -> ihre Destruktoren werden aufgerufen. Während dieses Vorgangs ist ein weiterer Ausnahmefall nicht zulässig, da nicht zwei Ausnahmen gleichzeitig behandelt werden können. Dies führt zu einem Aufruf von abort (), das Programm stürzt ab und die Steuerung kehrt zum Betriebssystem zurück.


1
Können Sie bitte erläutern, wie abort () in der obigen Situation aufgerufen wurde? Bedeutet, dass die Kontrolle über die Ausführung noch beim C ++
Krishna Oza am

1
@Krishna_Oza: Ganz einfach: Wenn ein Fehler ausgelöst wird, überprüft der Code, der einen Fehler auslöst, ein Bit, das anzeigt, dass das Laufzeitsystem gerade den Stapel abwickelt (dh einen anderen behandelt, throwaber noch keinen catchBlock dafür gefunden hat). In diesem Fall wird std::terminate(nicht abort) aufgerufen, anstatt eine (neue) Ausnahme auszulösen (oder das Abwickeln des Stapels fortzusetzen).
Marc van Leeuwen

53

Wir müssen hier differenzieren , anstatt blindlings allgemeinen Ratschlägen für bestimmte Fälle zu folgen .

Beachten Sie, dass im Folgenden das Problem von Containern mit Objekten ignoriert wird und was angesichts mehrerer Objekttypen in Containern zu tun ist. (Und es kann teilweise ignoriert werden, da einige Objekte einfach nicht gut in einen Container passen.)

Das ganze Problem wird leichter zu überlegen, wenn wir Klassen in zwei Typen aufteilen. Ein Klassenleiter kann zwei verschiedene Verantwortlichkeiten haben:

  • (R) Release-Semantik (auch bekannt als Free That Memory)
  • (C) Commit- Semantik (auch als Flush- Datei auf Festplatte bezeichnet)

Wenn wir die Frage so sehen, dann kann meiner Meinung nach argumentiert werden, dass (R) -Semantik niemals eine Ausnahme von einem dtor verursachen sollte, da es a) nichts gibt, was wir dagegen tun können, und b) viele Operationen mit freien Ressourcen dies nicht tun sorgen sogar für eine Fehlerprüfung, z .void free(void* p);

Objekte mit (C) -Semantik, wie ein Dateiobjekt, das seine Daten erfolgreich leeren muss, oder eine ("bereichsgeschützte") Datenbankverbindung, die ein Commit im dtor ausführt, sind von anderer Art: Wir können etwas gegen den Fehler tun (on die Anwendungsebene) und wir sollten wirklich nicht so weitermachen, als ob nichts passiert wäre.

Wenn wir der RAII-Route folgen und Objekte mit (C) -Semantik in ihren D'tors berücksichtigen, müssen wir meiner Meinung nach auch den seltsamen Fall berücksichtigen, in dem solche D'tors werfen können. Daraus folgt, dass Sie solche Objekte nicht in Container einfügen sollten, und es folgt auch, dass das Programm weiterhin kann, terminate()wenn ein Commit-dtor auslöst, während eine andere Ausnahme aktiv ist.


In Bezug auf Fehlerbehandlung (Commit / Rollback-Semantik) und Ausnahmen gibt es einen guten Vortrag von einem Andrei Alexandrescu : Fehlerbehandlung in C ++ / Deklarativer Kontrollfluss (gehalten auf der NDC 2014 )

Im Detail erklärt er, wie die Folly-Bibliothek eine UncaughtExceptionCounterfür ihre ScopeGuardWerkzeuge implementiert .

(Ich sollte beachten, dass andere auch ähnliche Ideen hatten.)

Während sich der Vortrag nicht auf das Werfen von einem D'tor konzentriert, zeigt er ein Werkzeug, das heute verwendet werden kann , um die Probleme mit dem Werfen von einem D'tor zu beseitigen.

In Zukunft gibt es möglicherweise eine Standardfunktion dafür, siehe N3614 , und eine Diskussion darüber .

Upd '17: Die C ++ 17- std::uncaught_exceptionsStandardfunktion hierfür ist afaikt. Ich werde den cppref-Artikel schnell zitieren:

Anmerkungen

Ein Beispiel, bei dem int-returning verwendet uncaught_exceptionswird, ist, dass zuerst ein Schutzobjekt erstellt und die Anzahl der nicht erfassten Ausnahmen in seinem Konstruktor aufgezeichnet wird. Die Ausgabe wird vom Destruktor des Schutzobjekts ausgeführt, es sei denn, foo () löst aus ( in diesem Fall ist die Anzahl der nicht erfassten Ausnahmen im Destruktor größer als vom Konstruktor beobachtet ).


6
Stimme voll und ganz zu. Und Hinzufügen einer weiteren semantischen (Ro) Rollback-Semantik. Wird häufig im Scope Guard verwendet. Wie in meinem Projekt, in dem ich ein ON_SCOPE_EXIT-Makro definiert habe. Der Fall der Rollback-Semantik ist, dass hier alles Sinnvolle passieren kann. Wir sollten den Fehler also wirklich nicht ignorieren.
Weipeng L

Ich denke, der einzige Grund, warum wir Commit-Semantik in Destruktoren haben, ist, dass C ++ nicht unterstützt finally.
user541686

@Mehrdad: finally ist ein dtor. Es heißt immer, egal was passiert. Zur syntaktischen Annäherung von finally siehe die verschiedenen Implementierungen von scope_guard. Heutzutage kann mit der vorhandenen Maschine (selbst im Standard ist es C ++ 14?), Um festzustellen, ob der dtor werfen darf, sogar eine absolute Sicherheit erreicht werden.
Martin Ba

1
@ MartinBa: Ich denke, Sie haben den Punkt meines Kommentars verpasst, was überraschend ist, da ich Ihrer Vorstellung zustimmte, dass (R) und (C) unterschiedlich sind. Ich habe versucht zu sagen, dass ein dtor von Natur aus ein Werkzeug für (R) und von finallyNatur aus ein Werkzeug für (C) ist. Wenn Sie nicht verstehen, warum: Überlegen Sie, warum es legitim ist, Ausnahmen in finallyBlöcken übereinander zu werfen , und warum dies nicht für Destruktoren gilt. (In gewissem Sinne handelt es sich um eine Daten- / Kontrollsache . Destruktoren dienen zum Freigeben von Daten, finallyzum Freigeben von Kontrolle. Sie sind unterschiedlich; es ist bedauerlich, dass C ++ sie zusammenhält.)
user541686

1
@Mehrdad: Hier wird es zu lang. Wenn Sie möchten, können Sie Ihre Argumente hier aufbauen: programmers.stackexchange.com/questions/304067/… . Vielen Dank.
Martin Ba

21

Die eigentliche Frage, die Sie sich stellen müssen, wenn Sie von einem Destruktor werfen, lautet: "Was kann der Anrufer damit machen?" Gibt es tatsächlich etwas Nützliches, das Sie mit der Ausnahme tun können, das die Gefahren ausgleichen würde, die durch das Werfen von einem Destruktor entstehen?

Was kann ich vernünftigerweise damit tun, wenn ich ein FooObjekt zerstöre und der FooDestruktor eine Ausnahme auslöst? Ich kann es protokollieren oder ignorieren. Das ist alles. Ich kann es nicht "reparieren", da das FooObjekt bereits verschwunden ist. Im besten Fall protokolliere ich die Ausnahme und fahre fort, als wäre nichts passiert (oder beende das Programm). Lohnt es sich wirklich, undefiniertes Verhalten durch Werfen von einem Destruktor zu verursachen?


11
Gerade bemerkt ... Werfen von einem dtor ist niemals undefiniertes Verhalten. Sicher, es könnte terminate () aufrufen, aber das ist ein sehr gut spezifiziertes Verhalten.
Martin Ba

4
std::ofstreamDer Destruktor wird geleert und schließt die Datei. Beim Leeren des Datenträgers kann ein Fehler auftreten, bei dem Sie unbedingt etwas Nützliches tun können: Zeigen Sie dem Benutzer einen Fehlerdialog, der besagt, dass auf dem Datenträger nicht mehr genügend Speicherplatz vorhanden ist.
Andy

13

Es ist gefährlich, aber es macht auch unter dem Gesichtspunkt der Lesbarkeit / Code-Verständlichkeit keinen Sinn.

Was Sie fragen müssen, ist in dieser Situation

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

Was soll die Ausnahme fangen? Sollte der Anrufer von foo? Oder sollte foo damit umgehen? Warum sollte sich der Anrufer von foo um ein Objekt innerhalb von foo kümmern? Es mag einen Weg geben, wie die Sprache dies definiert, um Sinn zu machen, aber es wird unlesbar und schwer zu verstehen sein.

Noch wichtiger ist, wohin geht der Speicher für Object? Wohin geht der Speicher, den das Objekt besaß? Ist es noch zugewiesen (angeblich, weil der Destruktor versagt hat)? Bedenken Sie auch, dass sich das Objekt im Stapelbereich befand , sodass es offensichtlich unabhängig davon verschwunden ist.

Dann betrachten Sie diesen Fall

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

Wenn das Löschen von obj3 fehlschlägt, wie lösche ich dann tatsächlich auf eine Weise, die garantiert nicht fehlschlägt? Es ist meine Erinnerung, verdammt!

Betrachten Sie nun im ersten Code-Snippet, dass Object automatisch verschwindet, da es sich auf dem Stapel befindet, während sich Object3 auf dem Heap befindet. Da der Zeiger auf Object3 weg ist, sind Sie eine Art SOL. Sie haben ein Speicherleck.

Ein sicherer Weg, Dinge zu tun, ist der folgende

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

Siehe auch diese FAQ


int foo()Wenn Sie diese Antwort wiederbeleben , können Sie im ersten Beispiel einen Funktions-Try-Block verwenden, um die gesamte Funktion foo in einen Try-Catch-Block zu verpacken, einschließlich der Catch-Destruktoren, wenn Sie dies möchten. Immer noch nicht der bevorzugte Ansatz, aber es ist eine Sache.
Tyree731

13

Aus dem ISO-Entwurf für C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

Daher sollten Destruktoren im Allgemeinen Ausnahmen abfangen und nicht zulassen, dass sie sich aus dem Destruktor ausbreiten.

3 Der Aufruf von Destruktoren für automatische Objekte, die auf dem Pfad von einem try-Block zu einem throw-Ausdruck erstellt wurden, wird als "Stack-Abwicklung" bezeichnet. [Hinweis: Wenn ein Destruktor, der beim Abwickeln des Stapels aufgerufen wird, mit einer Ausnahme beendet wird, wird std :: terminate aufgerufen (15.5.1). Daher sollten Destruktoren im Allgemeinen Ausnahmen abfangen und nicht zulassen, dass sie sich aus dem Destruktor ausbreiten. - Endnote]


1
Hat die Frage nicht beantwortet - das OP ist sich dessen bereits bewusst.
Arafangion

2
@Arafangion Ich bezweifle, dass er sich dessen bewusst war (std :: terminate wird aufgerufen), da die akzeptierte Antwort genau den gleichen Punkt machte.
Lothar

@Arafangion wie in einigen Antworten hier haben einige Leute erwähnt, dass abort () aufgerufen wird; Oder ruft std :: terminate abwechselnd die Funktion abort () auf?
Krishna Oza

7

Ihr Destruktor wird möglicherweise innerhalb einer Kette anderer Destruktoren ausgeführt. Das Auslösen einer Ausnahme, die nicht von Ihrem unmittelbaren Anrufer abgefangen wird, kann dazu führen, dass mehrere Objekte in einem inkonsistenten Zustand bleiben, wodurch noch mehr Probleme verursacht werden, als wenn der Fehler beim Bereinigungsvorgang ignoriert wird.


7

Ich gehöre zu der Gruppe, die der Ansicht ist, dass das in den Destruktor geworfene "Scoped Guard" -Muster in vielen Situationen nützlich ist - insbesondere für Unit-Tests. Beachten Sie jedoch, dass in C ++ 11 das Einwerfen eines Destruktors zu einem Aufruf von führt, std::terminateda Destruktoren implizit mit Anmerkungen versehen sind noexcept.

Andrzej Krzemieński hat einen großartigen Beitrag zum Thema Destruktoren, die werfen:

Er weist darauf hin, dass C ++ 11 einen Mechanismus hat, um den Standard noexceptfür Destruktoren zu überschreiben :

In C ++ 11 wird ein Destruktor implizit als angegeben noexcept. Auch wenn Sie keine Spezifikation hinzufügen und Ihren Destruktor folgendermaßen definieren:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

Der Compiler fügt noexceptIhrem Destruktor weiterhin unsichtbar Spezifikationen hinzu . Dies bedeutet, dass der Moment, in dem Ihr Destruktor eine Ausnahme std::terminateauslöst, aufgerufen wird, auch wenn keine Situation mit doppelten Ausnahmen vorliegt. Wenn Sie wirklich entschlossen sind, Ihren Destruktoren das Werfen zu erlauben, müssen Sie dies explizit angeben. Sie haben drei Möglichkeiten:

  • Geben Sie Ihren Destruktor explizit an als noexcept(false):
  • Erben Sie Ihre Klasse von einer anderen, die ihren Destruktor bereits als angibt noexcept(false).
  • Fügen Sie ein nicht statisches Datenelement in Ihre Klasse ein, das seinen Destruktor bereits als angibt noexcept(false).

Wenn Sie sich schließlich dazu entschließen, den Destruktor einzuschalten, sollten Sie sich immer des Risikos einer doppelten Ausnahme bewusst sein (Werfen, während der Stapel aufgrund einer Ausnahme abgewickelt wird). Dies würde einen Anruf bei verursachen std::terminateund es ist selten das, was Sie wollen. Um dieses Verhalten zu vermeiden, können Sie einfach überprüfen, ob bereits eine Ausnahme vorliegt, bevor Sie eine neue mit auslösen std::uncaught_exception().


6

Alle anderen haben erklärt, warum es schrecklich ist, Zerstörer zu werfen ... was können Sie dagegen tun? Wenn Sie einen Vorgang ausführen, der möglicherweise fehlschlägt, erstellen Sie eine separate öffentliche Methode, die eine Bereinigung durchführt und beliebige Ausnahmen auslösen kann. In den meisten Fällen ignorieren Benutzer dies. Wenn Benutzer den Erfolg / Misserfolg der Bereinigung überwachen möchten, können sie einfach die explizite Bereinigungsroutine aufrufen.

Beispielsweise:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};

Ich suche nach einer Lösung, aber sie versuchen zu erklären, was passiert ist und warum. Möchten Sie nur klarstellen, ob die Funktion close im Destruktor aufgerufen wird?
Jason Liu

5

Als Ergänzung zu den Hauptantworten, die gut, umfassend und genau sind, möchte ich zu dem Artikel, auf den Sie verweisen, einen Kommentar abgeben - der besagt, dass "Ausnahmen in Destruktoren zu werfen nicht so schlecht ist".

Der Artikel enthält die Zeile "Was sind die Alternativen zum Auslösen von Ausnahmen?" Und listet einige Probleme mit jeder der Alternativen auf. Nachdem wir dies getan haben, kommt wir zu dem Schluss, dass wir weiterhin Ausnahmen werfen sollten, da wir keine problemlose Alternative finden können.

Das Problem ist, dass keines der darin aufgeführten Probleme mit den Alternativen annähernd so schlimm ist wie das Ausnahmeverhalten, das, wie wir uns erinnern, "undefiniertes Verhalten Ihres Programms" ist. Einige der Einwände des Autors beinhalten "ästhetisch hässlich" und "schlechten Stil fördern". Was hätten Sie lieber? Ein Programm mit schlechtem Stil oder eines mit undefiniertem Verhalten?


1
Kein undefiniertes Verhalten, sondern sofortige Kündigung.
Marc van Leeuwen

Der Standard sagt "undefiniertes Verhalten". Dieses Verhalten ist häufig eine Beendigung, aber nicht immer.
DJClayworth

Nein, lesen Sie [außer.terminate] in Ausnahmebehandlung-> Sonderfunktionen (das ist 15.5.1 in meiner Kopie des Standards, aber seine Nummerierung ist wahrscheinlich veraltet).
Marc van Leeuwen

2

F: Meine Frage lautet also: Wenn das Werfen von einem Destruktor zu undefiniertem Verhalten führt, wie gehen Sie mit Fehlern um, die während eines Destruktors auftreten?

A: Es gibt verschiedene Möglichkeiten:

  1. Lassen Sie die Ausnahmen aus Ihrem Destruktor herausfließen, unabhängig davon, was anderswo vor sich geht. Und seien Sie sich dabei bewusst (oder sogar ängstlich), dass std :: terminate folgen kann.

  2. Lassen Sie niemals Ausnahmen aus Ihrem Destruktor herausfließen. Kann in ein Protokoll schreiben, ein großer roter schlechter Text, wenn Sie können.

  3. Mein Favorit : Wenn std::uncaught_exceptionfalse zurückgegeben wird, lassen Sie Ausnahmen herausfließen . Wenn true zurückgegeben wird, greifen Sie auf den Protokollierungsansatz zurück.

Aber ist es gut, d'tors hineinzuwerfen?

Ich stimme den meisten der oben genannten Punkte zu, dass das Werfen im Destruktor am besten vermieden wird, wo es sein kann. Aber manchmal ist es am besten zu akzeptieren, dass es passieren kann, und gut damit umzugehen. Ich würde 3 oben wählen.

Es gibt einige seltsame Fälle, in denen es eine großartige Idee ist , von einem Destruktor zu werfen. Wie der Fehlercode "muss überprüft werden". Dies ist ein Werttyp, der von einer Funktion zurückgegeben wird. Wenn der Aufrufer den enthaltenen Fehlercode liest / überprüft, wird der zurückgegebene Wert stillschweigend zerstört. Aber , wenn der zurückgegebene Fehlercode durch die Zeit nicht gelesen wurde geht die Rückgabewerte von Rahmen, wird es eine Ausnahme, wirft von seinem destructor .


4
Ihr Favorit ist etwas, das ich kürzlich ausprobiert habe, und es stellt sich heraus, dass Sie es nicht tun sollten . gotw.ca/gotw/047.htm
GManNickG

1

Ich folge derzeit der Richtlinie (die so viele sagen), dass Klassen keine aktiven Ausnahmen von ihren Destruktoren auslösen sollten, sondern stattdessen eine öffentliche "Schließen" -Methode bereitstellen sollten, um die Operation auszuführen, die fehlschlagen könnte ...

... aber ich glaube, dass Destruktoren für Klassen vom Containertyp wie ein Vektor keine Ausnahmen maskieren sollten, die von Klassen ausgelöst werden, die sie enthalten. In diesem Fall verwende ich tatsächlich eine "free / close" -Methode, die sich selbst rekursiv aufruft. Ja, sagte ich rekursiv. Es gibt eine Methode für diesen Wahnsinn. Die Weitergabe von Ausnahmen hängt davon ab, dass ein Stapel vorhanden ist: Wenn eine einzelne Ausnahme auftritt, werden beide verbleibenden Destruktoren weiterhin ausgeführt und die ausstehende Ausnahme wird weitergegeben, sobald die Routine zurückkehrt, was großartig ist. Wenn mehrere Ausnahmen auftreten, wird (je nach Compiler) entweder diese erste Ausnahme weitergegeben oder das Programm wird beendet, was in Ordnung ist. Wenn so viele Ausnahmen auftreten, dass die Rekursion den Stapel überläuft, stimmt etwas nicht, und jemand wird es herausfinden, was auch in Ordnung ist. Persönlich,

Der Punkt ist, dass der Container neutral bleibt und es an den enthaltenen Klassen liegt, zu entscheiden, ob sie sich im Hinblick auf das Auslösen von Ausnahmen von ihren Destruktoren verhalten oder schlecht verhalten.


1

Im Gegensatz zu Konstruktoren, bei denen das Auslösen von Ausnahmen ein nützlicher Weg sein kann, um anzuzeigen, dass die Objekterstellung erfolgreich war, sollten Ausnahmen nicht in Destruktoren ausgelöst werden.

Das Problem tritt auf, wenn während des Abwickelns des Stapels eine Ausnahme von einem Destruktor ausgelöst wird. In diesem Fall befindet sich der Compiler in einer Situation, in der er nicht weiß, ob er den Stapelabwicklungsprozess fortsetzen oder die neue Ausnahme behandeln soll. Das Endergebnis ist, dass Ihr Programm sofort beendet wird.

Folglich ist die beste Vorgehensweise, auf Ausnahmen bei Destruktoren insgesamt zu verzichten. Schreiben Sie stattdessen eine Nachricht in eine Protokolldatei.


1
Das Schreiben einer Nachricht in die Protokolldatei kann eine Ausnahme verursachen.
Konard

1

Martin Ba (oben) ist auf dem richtigen Weg - Sie architektonisch anders für RELEASE- und COMMIT-Logik.

Zur Veröffentlichung:

Sie sollten alle Fehler essen. Sie geben Speicher frei, schließen Verbindungen usw. Niemand im System sollte diese Dinge jemals wieder sehen, und Sie geben Ressourcen an das Betriebssystem zurück. Wenn es so aussieht, als ob Sie hier eine echte Fehlerbehandlung benötigen, ist dies wahrscheinlich eine Folge von Konstruktionsfehlern in Ihrem Objektmodell.

Für Commit:

Hier möchten Sie die gleiche Art von RAII-Wrapper-Objekten, die Dinge wie std :: lock_guard für Mutexe bereitstellen. Mit denen setzen Sie die Commit-Logik überhaupt nicht in den dtor. Sie haben eine dedizierte API dafür, dann Wrapper-Objekte, die RAII in IHRE Dtoren festschreiben und die Fehler dort behandeln. Denken Sie daran, dass Sie Ausnahmen in einem Destruktor problemlos abfangen können. es ist tödlich, sie herauszugeben. Auf diese Weise können Sie auch Richtlinien und unterschiedliche Fehlerbehandlungen implementieren, indem Sie einen anderen Wrapper erstellen (z. B. std :: unique_lock vs. std :: lock_guard), und sicherstellen, dass Sie nicht vergessen, die Festschreibungslogik aufzurufen - dies ist der einzige halbe Weg anständige Rechtfertigung dafür, es an erster Stelle in einen dtor zu setzen.


1

Meine Frage lautet also: Wenn das Werfen von einem Destruktor zu undefiniertem Verhalten führt, wie gehen Sie mit Fehlern um, die während eines Destruktors auftreten?

Das Hauptproblem ist folgendes: Sie können nicht scheitern, um zu scheitern . Was bedeutet es schließlich, nicht zu scheitern? Was passiert mit der Integrität unserer Daten, wenn das Festschreiben einer Transaktion in eine Datenbank fehlschlägt und nicht fehlschlägt (Rollback fehlschlägt)?

Da Destruktoren sowohl für normale als auch für außergewöhnliche (fehlgeschlagene) Pfade aufgerufen werden, können sie selbst nicht fehlschlagen, oder wir "versagen nicht".

Dies ist ein konzeptionell schwieriges Problem, aber häufig besteht die Lösung darin, nur einen Weg zu finden, um sicherzustellen, dass ein Fehlschlag nicht fehlschlagen kann. Beispielsweise kann eine Datenbank Änderungen schreiben, bevor sie in eine externe Datenstruktur oder Datei übernommen wird. Wenn die Transaktion fehlschlägt, kann die Datei- / Datenstruktur weggeworfen werden. Alles, was es dann sicherstellen muss, ist, dass das Festschreiben der Änderungen von dieser externen Struktur / Datei eine atomare Transaktion ist, die nicht fehlschlagen kann.

Die pragmatische Lösung besteht vielleicht darin, sicherzustellen, dass die Wahrscheinlichkeit eines Scheiterns astronomisch unwahrscheinlich ist, da es in einigen Fällen fast unmöglich sein kann, Dinge unmöglich zu machen, um zu scheitern.

Die für mich am besten geeignete Lösung besteht darin, Ihre Nicht-Bereinigungslogik so zu schreiben, dass die Bereinigungslogik nicht fehlschlagen kann. Wenn Sie beispielsweise versucht sind, eine neue Datenstruktur zu erstellen, um eine vorhandene Datenstruktur zu bereinigen, sollten Sie diese Hilfsstruktur möglicherweise im Voraus erstellen, damit wir sie nicht mehr in einem Destruktor erstellen müssen.

Das ist zwar viel leichter gesagt als getan, aber es ist der einzig richtige Weg, den ich sehe. Manchmal denke ich, dass es die Möglichkeit geben sollte, eine separate Destruktorlogik für normale Ausführungspfade zu schreiben, die von außergewöhnlichen entfernt ist, da Destruktoren manchmal das Gefühl haben, dass sie die doppelte Verantwortung haben, wenn sie versuchen, mit beiden umzugehen (ein Beispiel sind Scope Guards, die eine explizite Entlassung erfordern ; sie würden dies nicht benötigen, wenn sie außergewöhnliche Zerstörungspfade von nicht außergewöhnlichen unterscheiden könnten).

Das ultimative Problem ist jedoch, dass wir nicht versagen können, und es ist ein schwieriges Konzeptentwurfsproblem, das in allen Fällen perfekt zu lösen ist. Es wird einfacher, wenn Sie sich nicht zu sehr in komplexe Kontrollstrukturen mit Tonnen von kleinen Objekten einwickeln, die miteinander interagieren, und stattdessen Ihre Entwürfe etwas voluminöser modellieren (Beispiel: Partikelsystem mit einem Destruktor, um das gesamte Partikel zu zerstören System, kein separater nicht trivialer Destruktor pro Partikel). Wenn Sie Ihre Entwürfe auf dieser gröberen Ebene modellieren, müssen Sie sich mit weniger nicht trivialen Destruktoren befassen und können sich häufig den erforderlichen Speicher- / Verarbeitungsaufwand leisten, um sicherzustellen, dass Ihre Destruktoren nicht ausfallen können.

Und das ist natürlich eine der einfachsten Lösungen, Destruktoren seltener zu verwenden. Im obigen Partikelbeispiel sollten möglicherweise beim Zerstören / Entfernen eines Partikels einige Dinge getan werden, die aus irgendeinem Grund fehlschlagen können. In diesem Fall wird stattdessen eine solche Logik durch die Partikel des dtor von Aufrufen , die in einem außergewöhnlichen Weg durchgeführt werden könnte, könnte man stattdessen haben sie alle durch die Partikel - System durchgeführt , wenn es beseitigt ein Teilchen. Das Entfernen eines Partikels kann immer auf einem nicht außergewöhnlichen Weg erfolgen. Wenn das System zerstört wird, kann es möglicherweise nur alle Partikel entfernen und sich nicht mit der einzelnen Partikelentfernungslogik befassen, die fehlschlagen kann, während die Logik, die fehlschlagen kann, nur während der normalen Ausführung des Partikelsystems ausgeführt wird, wenn ein oder mehrere Partikel entfernt werden.

Es gibt oft solche Lösungen, die auftauchen, wenn Sie vermeiden, mit vielen kleinen Objekten mit nicht trivialen Destruktoren umzugehen. Wo Sie sich in einem Chaos verfangen können, in dem es fast unmöglich erscheint, Ausnahmesicherheit zu haben, ist, wenn Sie sich in vielen kleinen Objekten verheddern, die alle nicht triviale Dtoren haben.

Es wäre sehr hilfreich, wenn nothrow / noexcept tatsächlich in einen Compilerfehler übersetzt würde, wenn irgendetwas, das es angibt (einschließlich virtueller Funktionen, die die noexcept-Spezifikation seiner Basisklasse erben sollten), versuchen würde, alles aufzurufen, was auslösen könnte. Auf diese Weise könnten wir all diese Dinge zur Kompilierungszeit abfangen, wenn wir tatsächlich versehentlich einen Destruktor schreiben, der werfen könnte.


1
Zerstörung ist jetzt Misserfolg?
Neugieriger

Ich denke, er meint, dass Destruktoren während eines Fehlers aufgerufen werden, um diesen Fehler zu bereinigen. Wenn also ein Destruktor während einer aktiven Ausnahme aufgerufen wird, kann er nicht von einem vorherigen Fehler bereinigt werden.
user2445507

0

Stellen Sie ein Alarmereignis ein. In der Regel sind Alarmereignisse eine bessere Form, um Fehler beim Bereinigen von Objekten zu melden

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.