Sollte ich in C ++ einen Ausnahmespezifizierer verwenden?


123

In C ++ können Sie mithilfe eines Ausnahmespezifizierers angeben, dass eine Funktion eine Ausnahme auslösen kann oder nicht. Beispielsweise:

void foo() throw(); // guaranteed not to throw an exception
void bar() throw(int); // may throw an exception of type int
void baz() throw(...); // may throw an exception of some unspecified type

Ich bin aus folgenden Gründen zweifelhaft, ob ich sie tatsächlich verwenden soll:

  1. Der Compiler erzwingt Ausnahmespezifizierer nicht wirklich auf strenge Weise, daher sind die Vorteile nicht groß. Idealerweise möchten Sie einen Kompilierungsfehler erhalten.
  2. Wenn eine Funktion einen Ausnahmespezifizierer verletzt, besteht das Standardverhalten darin, das Programm zu beenden.
  3. In VS.Net wird throw (X) als throw (...) behandelt, sodass die Einhaltung des Standards nicht stark ist.

Denken Sie, dass Ausnahmespezifizierer verwendet werden sollten?
Bitte antworten Sie mit "Ja" oder "Nein" und begründen Sie Ihre Antwort.


7
"throw (...)" ist kein Standard-C ++. Ich glaube, es ist eine Erweiterung in einigen Compilern und hat im Allgemeinen die gleiche Bedeutung wie keine Ausnahmespezifikation.
Richard Corden

Antworten:


97

Nein.

Hier sind einige Beispiele warum:

  1. Vorlagencode kann mit Ausnahmespezifikationen nicht geschrieben werden.

    template<class T>
    void f( T k )
    {
         T x( k );
         x.x();
    }

    Die Kopien werden möglicherweise ausgelöst, die Parameterübergabe wird möglicherweise ausgelöst und x()es wird eine unbekannte Ausnahme ausgelöst.

  2. Ausnahmespezifikationen verbieten tendenziell die Erweiterbarkeit.

    virtual void open() throw( FileNotFound );

    könnte sich entwickeln in

    virtual void open() throw( FileNotFound, SocketNotReady, InterprocessObjectNotImplemented, HardwareUnresponsive );

    Das könnte man wirklich so schreiben

    throw( ... )

    Das erste ist nicht erweiterbar, das zweite ist überambitioniert und das dritte ist wirklich das, was Sie meinen, wenn Sie virtuelle Funktionen schreiben.

  3. Legacy-Code

    Wenn Sie Code schreiben, der auf einer anderen Bibliothek basiert, wissen Sie nicht genau, was er tun kann, wenn etwas schrecklich schief geht.

    int lib_f();
    
    void g() throw( k_too_small_exception )
    { 
       int k = lib_f();
       if( k < 0 ) throw k_too_small_exception();
    }

    gwird beendet, wenn lib_f()Würfe. Dies ist (in den meisten Fällen) nicht das, was Sie wirklich wollen. std::terminate()sollte niemals angerufen werden. Es ist immer besser, die Anwendung mit einer nicht behandelten Ausnahme abstürzen zu lassen, von der Sie einen Stack-Trace abrufen können, als still / gewaltsam zu sterben.

  4. Schreiben Sie Code, der häufige Fehler zurückgibt und in Ausnahmefällen auslöst.

    Error e = open( "bla.txt" );
    if( e == FileNotFound )
        MessageUser( "File bla.txt not found" );
    if( e == AccessDenied )
        MessageUser( "Failed to open bla.txt, because we don't have read rights ..." );
    if( e != Success )
        MessageUser( "Failed due to some other error, error code = " + itoa( e ) );
    
    try
    {
       std::vector<TObj> k( 1000 );
       // ...
    }
    catch( const bad_alloc& b )
    { 
       MessageUser( "out of memory, exiting process" );
       throw;
    }

Wenn Ihre Bibliothek jedoch nur Ihre eigenen Ausnahmen auslöst, können Sie Ausnahmespezifikationen verwenden, um Ihre Absicht anzugeben.


1
in 3 wäre es technisch std :: unerwartet nicht std :: terminate. Wenn diese Funktion aufgerufen wird, ruft die Standardeinstellung abort () auf. Dies erzeugt einen Core Dump. Wie ist das schlimmer als eine unbehandelte Ausnahme? (was im Grunde das gleiche tut)
Greg Rogers

6
@ Greg Rogers: Eine nicht abgefangene Ausnahme führt immer noch zum Abwickeln des Stapels. Dies bedeutet, dass Destruktoren aufgerufen werden. Und in diesen Destruktoren kann viel getan werden, wie: Ressourcen richtig freigegeben, Protokolle korrekt geschrieben, anderen Prozessen wird mitgeteilt, dass der aktuelle Prozess abstürzt usw. Zusammenfassend ist es RAII.
Paercebal

Sie haben ausgelassen: Dadurch wird effektiv alles in ein try {...} catch (<specified exceptions>) { <do whatever> } catch (...) { unexpected(); ]Konstrukt eingeschlossen, unabhängig davon, ob Sie dort einen try-Block möchten oder nicht.
David Thornley

4
@paercebal Das ist falsch. In der Implementierung wird definiert, ob Destruktoren für nicht erfasste Ausnahmen ausgeführt werden. In den meisten Umgebungen werden die Stack / Run-Destruktoren nicht abgewickelt, wenn die Ausnahme nicht abgefangen wird. Wenn Sie portabel sicherstellen möchten, dass Ihre Destruktoren auch dann ausgeführt werden, wenn eine Ausnahme ausgelöst und nicht behandelt wird (dies ist von zweifelhaftem Wert), müssen Sie Ihren Code wie try { <<...code...>> } catch(...) /* stack guaranteed to be unwound here and dtors run */ { throw; /* pass it on to the runtime */ }
folgt

" Es ist immer besser, die Anwendung mit einer nicht behandelten Ausnahme abstürzen zu lassen, von der Sie einen Stack-Trace abrufen können, als still / gewaltsam zu sterben. " Wie kann ein "Absturz" besser sein als ein sauberer Aufruf an terminate()? Warum nicht einfach anrufen abort()?
Neugieriger

42

Vermeiden Sie Ausnahmespezifikationen in C ++. Die Gründe, die Sie in Ihrer Frage angeben, sind ein ziemlich guter Anfang für das Warum.

Siehe Herb Sutters "Ein pragmatischer Blick auf Ausnahmespezifikationen" .


3
@awoodland: Die Verwendung von 'Dynamic-Exception-Specifications' ( throw(optional-type-id-list)) ist in C ++ 11 veraltet. Sie sind immer noch im Standard, aber ich denke, es wurde ein Warnschuss gesendet, dass ihre Verwendung sorgfältig abgewogen werden sollte. C ++ 11 fügt die noexceptSpezifikation und den Operator hinzu. Ich weiß nicht genug Details, um es noexceptzu kommentieren. Dieser Artikel scheint ziemlich detailliert zu sein: akrzemi1.wordpress.com/2011/06/10/using-noexcept Und Dietmar Kühl hat einen Artikel im Juni 2011 Overload Journal: accu.org/var/uploads/journals/overload103.pdf
Michael Burr

@MichaelBurr Only throw(something)gilt als nutzlos und eine schlechte Idee. throw()ist nützlich.
Neugieriger

" Viele Leute glauben, dass Ausnahmespezifikationen Folgendes bewirken: Aufzählungszeichen Garantieren Sie, dass Funktionen nur aufgelistete Ausnahmen auslösen (möglicherweise keine). Aufzählungszeichen Aktivieren Sie Compiler-Optimierungen basierend auf dem Wissen, dass nur aufgelistete Ausnahmen (möglicherweise keine) ausgelöst werden. Die oben genannten Erwartungen sind: wieder täuschend nahe daran, korrekt zu sein "Nein, die oben genannten Erwartungen sind absolut korrekt.
Neugieriger

14

Ich denke, dass die Standardausnahmespezifizierer mit Ausnahme der Konvention (für C ++)
ein Experiment im C ++ - Standard waren, das größtenteils fehlgeschlagen ist.
Die Ausnahme besteht darin, dass der No-Throw-Bezeichner nützlich ist. Sie sollten jedoch auch intern den entsprechenden try catch-Block hinzufügen, um sicherzustellen, dass der Code mit dem Bezeichner übereinstimmt. Herb Sutter hat eine Seite zu diesem Thema. Gotch 82

Darüber hinaus halte ich es für sinnvoll, Ausnahmegarantien zu beschreiben.

Hierbei handelt es sich im Wesentlichen um eine Dokumentation darüber, wie der Status eines Objekts von Ausnahmen beeinflusst wird, die einer Methode für dieses Objekt entgehen. Leider werden sie vom Compiler nicht erzwungen oder anderweitig erwähnt.
Boost und Ausnahmen

Ausnahmegarantien

Keine Garantie:

Es gibt keine Garantie für den Status des Objekts, nachdem eine Ausnahme einer Methode entgangen ist.
In diesen Situationen sollte das Objekt nicht mehr verwendet werden.

Grundgarantie:

In fast allen Situationen sollte dies die Mindestgarantie sein, die eine Methode bietet.
Dies garantiert, dass der Status des Objekts genau definiert ist und weiterhin konsistent verwendet werden kann.

Starke Garantie: (auch bekannt als Transaktionsgarantie)

Dies garantiert, dass die Methode erfolgreich abgeschlossen wird.
Andernfalls wird eine Ausnahme ausgelöst und der Objektstatus wird nicht geändert.

Keine Wurfgarantie:

Die Methode garantiert, dass keine Ausnahmen aus der Methode übertragen werden dürfen.
Alle Zerstörer sollten diese Garantie geben.
| NB Wenn eine Ausnahme einem Destruktor entgeht, während sich eine Ausnahme bereits ausbreitet
| Die Anwendung wird beendet


Die Garantien sind etwas, das jeder C ++ - Programmierer wissen muss, aber sie scheinen mir nicht mit Ausnahmespezifikationen in Zusammenhang zu stehen.
David Thornley

1
@ David Thornley: Ich sehe die Garantien als das, was die Ausnahmespezifikationen hätten sein sollen (dh eine Methode mit dem starken G kann keine Methode mit einem grundlegenden G ohne Schutz aufrufen). Leider bin ich mir nicht sicher, ob sie gut genug definiert sind, um vom Compiler auf nützliche Weise durchgesetzt zu werden.
Martin York

" Viele Leute glauben, dass Ausnahmespezifikationen Folgendes bewirken: - Stellen Sie sicher, dass Funktionen nur aufgelistete Ausnahmen auslösen (möglicherweise keine). - Aktivieren Sie Compiler-Optimierungen basierend auf dem Wissen, dass nur aufgelistete Ausnahmen (möglicherweise keine) ausgelöst werden. Die oben genannten Erwartungen sind: wieder täuschend nahe daran, richtig zu sein. "Eigentlich sind beide genau richtig.
Neugieriger

@curiousguy. So macht es Java, weil es beim Kompilieren überprüft. C ++ sind Laufzeitprüfungen. Also : Guarantee that functions will only throw listed exceptions (possibly none). Nicht wahr. Es wird nur garantiert, dass die Anwendung beendet wird, wenn die Funktion diese Ausnahmen auslöst. Enable compiler optimizations based on the knowledge that only listed exceptions (possibly none) will be thrownKann das nicht machen. Weil ich gerade darauf hingewiesen habe, können Sie nicht garantieren, dass die Ausnahme nicht ausgelöst wird.
Martin York

1
@LokiAstari "So macht es Java, weil es beim Kompilieren überprüft wird_" Die Java-Ausnahmespezifikation ist ein fehlgeschlagenes Experiment. Java hat nicht die nützlichste Ausnahmespezifikation: throw()(wirft nicht). " Es wird nur garantiert, dass die Anwendung beendet wird, wenn die Funktion diese Ausnahmen auslöst. " Nein "nicht wahr" bedeutet wahr. In C ++ gibt es keine Garantie dafür, dass eine Funktion terminate()niemals aufgerufen wird. " Weil ich gerade darauf hingewiesen habe, dass Sie nicht garantieren können, dass die Ausnahme nicht ausgelöst wird. " Sie garantieren, dass die Funktion nicht ausgelöst wird. Welches ist genau das, was Sie brauchen.
Neugieriger

8

gcc gibt Warnungen aus, wenn Sie Ausnahmespezifikationen verletzen. Ich verwende Makros, um die Ausnahmespezifikationen nur in einem "Flusen" -Modus zu verwenden, der ausdrücklich zur Überprüfung kompiliert wird, um sicherzustellen, dass die Ausnahmen mit meiner Dokumentation übereinstimmen.


7

Der einzige nützliche Ausnahmespezifizierer ist "throw ()", wie in "wirft nicht".


2
Können Sie bitte einen Grund hinzufügen, warum es nützlich ist?
Buti-Oxa

2
Warum ist es nicht nützlich? Es gibt nichts Nützlicheres, als zu wissen, dass eine Funktion keine Ausnahmen von links nach rechts und in der Mitte auslöst.
Matt Joiner

1
Eine ausführliche Erklärung finden Sie in der Diskussion zu Herb Sutter, auf die in der Antwort von Michael Burr verwiesen wird.
Harold Ekstrom

4

Ausnahmespezifikationen sind in C ++ keine wunderbar nützlichen Werkzeuge. Es gibt jedoch eine gute Verwendung für sie, wenn sie mit std :: unerwartet kombiniert werden.

In einigen Projekten mache ich Code mit Ausnahmespezifikationen und rufe dann set_unexpected () mit einer Funktion auf, die eine spezielle Ausnahme meines eigenen Designs auslöst. Diese Ausnahme erhält bei der Erstellung eine plattformspezifische Rückverfolgung und wird von std :: bad_exception abgeleitet (damit sie bei Bedarf weitergegeben werden kann). Wenn es wie üblich zu einem terminate () -Aufruf kommt, wird die Rückverfolgung durch what () (sowie die ursprüngliche Ausnahme, die sie verursacht hat; nicht zu schwer zu finden) gedruckt, und ich erhalte Informationen darüber, wo sich mein Vertrag befand verletzt, z. B. welche unerwartete Bibliotheksausnahme ausgelöst wurde.

Wenn ich dies tue, erlaube ich niemals die Weitergabe von Bibliotheksausnahmen (außer Standardausnahmen) und leite alle meine Ausnahmen von std :: exception ab. Wenn sich eine Bibliothek zum Werfen entscheidet, werde ich abfangen und in meine eigene Hierarchie konvertieren, sodass ich den Code immer kontrollieren kann. Vorlagenfunktionen, die abhängige Funktionen aufrufen, sollten aus offensichtlichen Gründen Ausnahmespezifikationen vermeiden. Es ist jedoch sowieso selten, dass eine Vorlagenfunktionsschnittstelle mit Bibliothekscode vorhanden ist (und nur wenige Bibliotheken verwenden Vorlagen wirklich auf nützliche Weise).


3

Wenn Sie Code schreiben, der von Personen verwendet wird, die sich lieber die Funktionsdeklaration als Kommentare dazu ansehen, gibt eine Spezifikation an, welche Ausnahmen sie möglicherweise abfangen möchten.

Ansonsten finde ich es nicht besonders nützlich, etwas anderes zu verwenden, als throw()anzuzeigen, dass es keine Ausnahmen auslöst.


3

Nein. Wenn Sie sie verwenden und eine Ausnahme ausgelöst wird, die Sie weder durch Ihren Code noch durch Ihren Code angegeben haben, besteht das Standardverhalten darin, Ihr Programm sofort zu beenden.

Ich glaube auch, dass ihre Verwendung in aktuellen Entwürfen des C ++ 0x-Standards veraltet ist.



2

Im Allgemeinen würde ich keine Ausnahmespezifizierer verwenden. In Fällen, in denen eine andere Ausnahme von der fraglichen Funktion kommen sollte, die das Programm definitiv nicht korrigieren kann , kann dies nützlich sein. Stellen Sie in jedem Fall sicher, dass Sie klar dokumentieren, welche Ausnahmen von dieser Funktion zu erwarten sind.

Ja, das erwartete Verhalten einer nicht angegebenen Ausnahme, die von einer Funktion mit Ausnahmespezifizierern ausgelöst wird, besteht darin, terminate () aufzurufen.

Ich werde auch bemerken, dass Scott Meyers dieses Thema in More Effective C ++ anspricht. Sein Effective C ++ und More Effective C ++ sind sehr empfehlenswerte Bücher.


2

Ja, wenn Sie sich für interne Dokumentation interessieren. Oder schreiben Sie eine Bibliothek, die andere verwenden, damit sie erkennen können, was passiert, ohne die Dokumentation zu konsultieren. Werfen oder nicht werfen kann als Teil der API betrachtet werden, fast wie der Rückgabewert.

Ich stimme zu, sie sind nicht wirklich nützlich, um die Korrektheit des Java-Stils im Compiler durchzusetzen, aber es ist besser als nichts oder zufällige Kommentare.


2

Sie können für Komponententests nützlich sein, damit Sie beim Schreiben von Tests wissen, was die Funktion erwartet, wenn sie fehlschlägt, aber im Compiler gibt es keine Durchsetzung, die sie umgibt. Ich denke, dass es sich um zusätzlichen Code handelt, der in C ++ nicht erforderlich ist. Was auch immer Sie auswählen, alles, worauf Sie sich verlassen sollten, ist, dass Sie im gesamten Projekt und in den Teammitgliedern denselben Codierungsstandard befolgen, damit Ihr Code lesbar bleibt.


0

Aus dem Artikel:

http://www.boost.org/community/exception_safety.html

"Es ist bekannt, dass es unmöglich ist, einen ausnahmesicheren generischen Container zu schreiben." Diese Behauptung wird häufig unter Bezugnahme auf einen Artikel von Tom Cargill [4] gehört, in dem er das Problem der Ausnahmesicherheit für eine generische Stapelvorlage untersucht. In seinem Artikel wirft Cargill viele nützliche Fragen auf, präsentiert aber leider keine Lösung für sein Problem.1 Abschließend schlägt er vor, dass eine Lösung möglicherweise nicht möglich ist. Leider wurde sein Artikel von vielen als „Beweis“ für diese Spekulation gelesen. Seit seiner Veröffentlichung gab es viele Beispiele für ausnahmesichere generische Komponenten, darunter die C ++ - Standardbibliothekscontainer.

Und tatsächlich kann ich mir Möglichkeiten vorstellen, um Vorlagenklassen ausnahmesicher zu machen. Wenn Sie nicht die Kontrolle über alle Unterklassen haben, haben Sie möglicherweise trotzdem ein Problem. Zu diesem Zweck können Sie in Ihren Klassen Typedefs erstellen, die die von verschiedenen Vorlagenklassen ausgelösten Ausnahmen definieren. Ich denke, das Problem besteht darin, es wie immer danach anzugehen, anstatt es von Anfang an zu entwerfen, und ich denke, es ist dieser Aufwand, der die wahre Hürde darstellt.


-2

Ausnahmespezifikationen = Müll, fragen Sie jeden Java-Entwickler über 30 Jahre


8
Java-Programmierer über 30 sollten sich schlecht fühlen, wenn sie mit C nicht umgehen können. Sie haben keine Entschuldigung, Java zu verwenden.
Matt Joiner
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.