Soll ich von std :: exception erben?

98

Ich habe gesehen, dass mindestens eine zuverlässige Quelle (eine von mir verwendete C ++ - Klasse) empfohlen hat, von anwendungsspezifischen Ausnahmeklassen in C ++ zu erben std::exception. Die Vorteile dieses Ansatzes sind mir nicht klar.

In C # liegen die Gründe für das Erben auf der ApplicationExceptionHand: Sie erhalten eine Handvoll nützlicher Methoden, Eigenschaften und Konstruktoren und müssen nur das hinzufügen oder überschreiben, was Sie benötigen. Mit std::exceptionscheint es , dass alles , was man eine bekommen , ist what()Methode zu überschreiben, die Sie könnten genauso gut selbst erstellen.

Welche Vorteile bietet die Verwendung std::exceptionals Basisklasse für meine anwendungsspezifische Ausnahmeklasse , wenn überhaupt ? Gibt es gute Gründe, nicht zu erben std::exception?

John M Gant
quelle
Vielleicht möchten Sie einen Blick darauf werfen
sbi
2
Obwohl es sich um eine Randnotiz handelt, die nichts mit der jeweiligen Frage zu tun hat , müssen C ++ - Klassen, die Sie belegen, nicht unbedingt verlässliche Quellen für bewährte Verfahren sein.
Christian Rau

Antworten:

69

Der Hauptvorteil ist, dass Code, der Ihre Klassen verwendet, nicht genau wissen muss, was Sie throwtun, sondern nur catchdie std::exception.

Bearbeiten: Wie Martin und andere angemerkt haben, möchten Sie tatsächlich von einer der std::exceptionim <stdexcept>Header deklarierten Unterklassen ableiten .

Nikolai Fetissov
quelle
21
Es gibt keine Möglichkeit, eine Nachricht an std :: exception zu übergeben. std :: runtime_error akzeptiert eine Zeichenfolge und wird von std :: exception abgeleitet.
Martin York
14
Sie sollten keine Nachricht an den Ausnahmetypkonstruktor übergeben (beachten Sie, dass Nachrichten lokalisiert werden müssen). Definieren Sie stattdessen einen Ausnahmetyp, der den Fehler semantisch kategorisiert, und speichern Sie im Ausnahmeobjekt, was Sie zum Formatieren einer benutzerfreundlichen Nachricht benötigen , dann an der Fangstelle.
Emil
std::exceptionwird in der Kopfzeile definiert<exception> ( Beweis ). -1
Alexander Shukaev
5
Also, was ist dein Punkt? Definition impliziert Erklärung, was sehen Sie hier falsch?
Nikolai Fetissov
40

Das Problem dabei std::exceptionist, dass es keinen Konstruktor (in den Standard-kompatiblen Versionen) gibt, der eine Nachricht akzeptiert.

Daher leite ich lieber ab std::runtime_error. Dies ist abgeleitet von, std::exceptionaber seine Konstruktoren ermöglichen es Ihnen, einen C-String oder a std::stringan den Konstruktor zu übergeben, der char const*beim what()Aufruf (als a ) zurückgegeben wird.

Martin York
quelle
11
Es ist keine gute Idee, eine benutzerfreundliche Nachricht zum Zeitpunkt des Wurfs zu formatieren, da dies Code auf niedrigerer Ebene mit Lokalisierungsfunktionen und so weiter koppeln würde. Speichern Sie stattdessen alle relevanten Informationen im Ausnahmeobjekt und lassen Sie die Catch-Site eine benutzerfreundliche Nachricht formatieren, die auf dem Ausnahmetyp und den darin enthaltenen Daten basiert.
Emil
16
@ Emil: Sicher, wenn Sie eine Ausnahme sind, tragen Sie vom Benutzer anzeigbare Nachrichten, obwohl diese im Allgemeinen nur zu Protokollierungszwecken dienen. Auf der Throw-Site haben Sie nicht den Kontext, um eine Benutzernachricht zu erstellen (da dies ohnehin eine Bibliothek sein kann). Die Fangstelle hat mehr Kontext und kann die entsprechende Nachricht generieren.
Martin York
3
Ich weiß nicht, woher Sie die Idee haben, dass Ausnahmen nur zu Protokollierungszwecken dienen. :)
Emil
1
@Emil: Wir haben dieses Argument bereits durchgearbeitet. Wo ich darauf hingewiesen habe, dass dies nicht das ist, was Sie in die Ausnahme setzen. Ihr Verständnis des Themas scheint gut zu sein, aber Ihre Argumentation ist fehlerhaft. Für den Benutzer generierte Fehlermeldungen unterscheiden sich vollständig von Protokollmeldungen für den Entwickler.
Martin York
1
@Emil. Diese Diskussion ist ein Jahr alt. Erstellen Sie an dieser Stelle Ihre eigene Frage. Ihre Argumente sind für mich einfach falsch (und aufgrund der Stimmen stimmen die Leute mir eher zu). Siehe Was ist eine gute Anzahl von Ausnahmen, die für meine Bibliothek implementiert werden müssen? und Ist es wirklich eine schlechte Sache, allgemeine Ausnahmen zu erwischen? Sie haben seit Ihrer letzten Diatribe keine neuen Informationen mehr bereitgestellt.
Martin York
17

Grund für das Erben von std::exceptionist die "Standard" -Basisklasse für Ausnahmen. Daher ist es für andere Teammitglieder selbstverständlich, dies zu erwarten und die Basis zu fangen std::exception.

Wenn Sie nach Bequemlichkeit suchen, können Sie von std::runtime_errordiesem std::stringKonstruktor erben .

Aleksei Potov
quelle
2
Es ist wahrscheinlich keine gute Idee, von einem anderen Standardausnahmetyp mit Ausnahme von std :: exception abzuleiten. Das Problem ist, dass sie nicht virtuell von std :: exception stammen, was zu subtilen Fehlern mit Mehrfachvererbung führen kann, bei denen ein catch (std :: exception &) aufgrund der Konvertierung in std :: exception möglicherweise keine Ausnahme abfangen kann mehrdeutig.
Emil
2
Ich habe den Artikel über Boost zu diesem Thema gelesen. Es scheint im rein logischen Sinne vernünftig. Aber ich habe MH in Ausnahmen in freier Wildbahn noch nie gesehen. Ich würde auch nicht in Betracht ziehen, MH in Ausnahmen zu verwenden, so dass es wie ein Vorschlaghammer erscheint, ein nicht existierendes Problem zu beheben. Wenn dies ein echtes Problem gewesen wäre, hätten wir sicher Maßnahmen des Normungsausschusses ergriffen, um einen so offensichtlichen Fehler zu beheben. Meine Meinung ist also, dass std :: runtime_error (und family) immer noch eine akzeptable Ausnahme sind, um sie zu werfen oder abzuleiten.
Martin York
5
@Emil: Es std::exceptionist eine ausgezeichnete Idee, sich von anderen Standardausnahmen abzuleiten . Was Sie nicht tun sollten, ist von mehr als einem zu erben .
Mooing Duck
@MooingDuck: Wenn Sie von mehr als einem Standardausnahmetyp abgeleitet sind, wird std :: exception mehrmals (nicht virtuell) abgeleitet. Siehe boost.org/doc/libs/release/libs/exception/doc/… .
Emil
1
@Emil: Das folgt aus dem, was ich gesagt habe.
Mooing Duck
13

Ich habe einmal an der Bereinigung einer großen Codebasis teilgenommen, in der die vorherigen Autoren Ints, HRESULTS, std :: string, char *, zufällige Klassen ... überall verschiedene Dinge geworfen hatten; Nennen Sie einfach einen Typ und er wurde wahrscheinlich irgendwo hingeworfen. Und überhaupt keine gemeinsame Basisklasse. Glauben Sie mir, die Dinge waren viel aufgeräumter, als wir den Punkt erreichten, dass alle geworfenen Typen eine gemeinsame Basis hatten, die wir fangen konnten und wussten, dass nichts vorbei kommen würde. Bitte tun Sie sich selbst (und denjenigen, die Ihren Code in Zukunft pflegen müssen) einen Gefallen und tun Sie dies von Anfang an so.

timday
quelle
12

Sie sollten von boost :: exception erben . Es bietet viel mehr Funktionen und gut verstandene Möglichkeiten, zusätzliche Daten zu übertragen. Wenn Sie Boost nicht verwenden , ignorieren Sie diesen Vorschlag.

James Schek
quelle
5
Beachten Sie jedoch, dass boost :: exception selbst nicht von std :: exception abgeleitet ist. Selbst wenn Sie von boost :: exception ableiten, sollten Sie auch von std :: exception ableiten (als separate Anmerkung, wenn Sie von Ausnahmetypen ableiten, sollten Sie die virtuelle Vererbung verwenden.)
Emil
10

Ja, Sie sollten ableiten von std::exception.

Andere haben geantwortet, dass std::exceptiondas Problem besteht, dass Sie keine Textnachricht an sie weitergeben können. Es ist jedoch im Allgemeinen keine gute Idee, zu versuchen, eine Benutzernachricht am Punkt des Wurfs zu formatieren. Verwenden Sie stattdessen das Ausnahmeobjekt, um alle relevanten Informationen zur Fangstelle zu transportieren, die dann eine benutzerfreundliche Nachricht formatieren kann.

Emil
quelle
6
+1, guter Punkt. Sowohl aus Sicht der Trennung als auch aus Sicht von i18n ist es definitiv besser, die Präsentationsschicht die Benutzermeldung erstellen zu lassen.
John M Gant
@ JohnMGant und Emil: Interessant, können Sie auf ein konkretes Beispiel verweisen, wie dies getan werden kann. Ich verstehe, dass man std::exceptiondie Informationen der Ausnahme ableiten und tragen kann . Aber wer ist für das Erstellen / Formatieren der Fehlermeldung verantwortlich? Die ::what()Funktion oder etwas anderes?
AlfC
1
Sie würden die Nachricht auf der Catch-Site formatieren, höchstwahrscheinlich auf Anwendungsebene (und nicht auf Bibliotheksebene). Sie fangen es nach Typ ab, suchen es dann nach Informationen und lokalisieren / formatieren die entsprechende Benutzernachricht.
Emil
9

Der Grund, warum Sie möglicherweise erben möchten, std::exceptionliegt darin, dass Sie eine Ausnahme auslösen können, die gemäß dieser Klasse abgefangen wird, z.

class myException : public std::exception { ... };
try {
    ...
    throw myException();
}
catch (std::exception &theException) {
    ...
}
SingleNegationElimination
quelle
6

Es gibt ein Problem mit der Vererbung, über das Sie Bescheid wissen sollten, nämlich das Aufteilen von Objekten. Wenn Sie throw e;einen Wurfausdruck schreiben , wird ein temporäres Objekt initialisiert, das als Ausnahmeobjekt bezeichnet wird und dessen Typ durch Entfernen aller Lebenslaufqualifizierer der obersten Ebene aus dem statischen Typ des Operanden von bestimmt wird throw. Das könnte nicht das sein, was Sie erwarten. Beispiel für ein Problem finden Sie hier .

Es ist kein Argument gegen Vererbung, es ist nur eine Information, die man wissen muss.

Kirill V. Lyadvinsky
quelle
3
Ich denke, das Mitnehmen ist das "wirf e;" ist böse und "werfen"; ist in Ordnung.
James Schek
2
Ja, throw;ist in Ordnung, aber es ist nicht offensichtlich, dass Sie so etwas schreiben sollten.
Kirill V. Lyadvinsky
1
Es ist besonders schmerzhaft für Java-Entwickler, bei denen das erneute Werfen mit "throw e" erfolgt.
James Schek
Betrachten Sie nur das Ableiten von abstrakten Basistypen. Das Abfangen eines abstrakten Basistyps nach Wert führt zu einem Fehler (Vermeiden des Aufschneidens). Wenn Sie einen abstrakten Basistyp als Referenz abfangen und dann versuchen, eine Kopie davon zu werfen, wird ein Fehler angezeigt (erneut wird das Schneiden vermieden.)
Emil
Sie können dieses Problem auch lösen, indem Sie eine Elementfunktion raise()als solche hinzufügen : virtual void raise() { throw *this; }. Denken Sie daran, es in jeder abgeleiteten Ausnahme zu überschreiben.
Justin Time - Wiedereinsetzung Monica
5

Unterschied: std :: runtime_error vs std :: exception ()

Ob Sie davon erben sollten oder nicht, liegt bei Ihnen. Standard std::exceptionund seine Standardnachkommen schlagen eine mögliche Ausnahmehierarchiestruktur (Unterteilung in logic_errorSubhierarchie und runtime_errorSubhierarchie) und eine mögliche Ausnahmeobjektschnittstelle vor. Wenn es dir gefällt - benutze es. Wenn Sie aus irgendeinem Grund etwas anderes benötigen, definieren Sie Ihr eigenes Ausnahmesystem.

Ameise
quelle
3

Da die Sprache bereits eine std :: Ausnahme auslöst, müssen Sie sie trotzdem abfangen, um eine anständige Fehlerberichterstattung zu ermöglichen. Sie können denselben Fang auch für alle unerwarteten Ausnahmen verwenden. Außerdem würde fast jede Bibliothek, die Ausnahmen auslöst, diese von std :: exception ableiten.

Mit anderen Worten, es ist entweder

catch (...) {cout << "Unknown exception"; }

oder

catch (const std::exception &e) { cout << "unexpected exception " << e.what();}

Und die zweite Option ist definitiv besser.


quelle
3

Wenn alle Ihre möglichen Ausnahmen davon herrühren std::exception, kann Ihr Fangblock einfach catch(std::exception & e)und sicher sein, alles zu erfassen.

Sobald Sie die Ausnahme erfasst haben, können Sie diese whatMethode verwenden, um weitere Informationen abzurufen. C ++ unterstützt keine Ententypisierung, daher whatwürde eine andere Klasse mit einer Methode einen anderen Fang und einen anderen Code erfordern, um sie zu verwenden.

Mark Ransom
quelle
7
Unterklasse nicht std :: exception und dann catch (std :: exception e). Sie müssen einen Verweis auf std :: exception & (oder einen Zeiger) abfangen, sonst schneiden Sie die Unterklassendaten ab.
jmucchiello
1
Das war ein dummer Fehler - sicherlich weiß ich es besser. stackoverflow.com/questions/1095225/…
Mark Ransom
3

Ob von einem Standardausnahmetyp abgeleitet werden soll oder nicht, ist die erste Frage. Auf diese Weise wird ein einziger Ausnahmebehandler für alle Standardbibliotheksausnahmen und Ihre eigenen aktiviert, es werden jedoch auch solche Catch-Them-All-Handler empfohlen. Das Problem ist, dass man nur Ausnahmen abfangen sollte, mit denen man umgehen kann. In main () ist es beispielsweise wahrscheinlich eine gute Sache, alle std :: -Ausnahmen abzufangen, wenn die Zeichenfolge what () vor dem Beenden als letzter Ausweg protokolliert wird. An anderer Stelle ist es jedoch unwahrscheinlich, dass dies eine gute Idee ist.

Wenn Sie sich entschieden haben, ob Sie von einem Standardausnahmetyp ableiten möchten oder nicht, stellt sich die Frage, welcher die Basis sein soll. Wenn Ihre Anwendung i18n nicht benötigt, ist das Formatieren einer Nachricht am Anrufstandort möglicherweise genauso gut wie das Speichern von Informationen und das Generieren der Nachricht am Anrufstandort. Das Problem ist, dass die formatierte Nachricht möglicherweise nicht benötigt wird. Verwenden Sie besser ein faules Nachrichtengenerierungsschema - möglicherweise mit vorab zugewiesenem Speicher. Wenn die Nachricht dann benötigt wird, wird sie beim Zugriff generiert (und möglicherweise im Ausnahmeobjekt zwischengespeichert). Wenn die Nachricht beim Auslösen generiert wird, wird daher ein std :: exception-Derivat wie std :: runtime_error als Basisklasse benötigt. Wenn die Nachricht träge generiert wird, ist std :: exception die geeignete Basis.

rauben
quelle
0

Ein weiterer Grund für Unterklassenausnahmen ist ein besserer Entwurfsaspekt bei der Arbeit an großen gekapselten Systemen. Sie können es für Dinge wie Validierungsnachrichten, Benutzerabfragen, schwerwiegende Controller-Fehler usw. wiederverwenden. Anstatt alle Ihre Validierungsnachrichten wie Nachrichten neu zu schreiben oder neu zu verknüpfen, können Sie sie einfach in der Hauptquelldatei "abfangen", aber den Fehler an einer beliebigen Stelle in Ihrer gesamten Klassengruppe auslösen.

Beispiel: Eine schwerwiegende Ausnahme beendet das Programm, ein Validierungsfehler löscht nur den Stapel und eine Benutzerabfrage stellt dem Endbenutzer eine Frage.

Auf diese Weise können Sie auch dieselben Klassen auf unterschiedlichen Schnittstellen wiederverwenden. Beispiel: Eine Windows-Anwendung kann ein Meldungsfeld verwenden, ein Webdienst zeigt HTML an und das Berichtssystem protokolliert es und so weiter.

James Burnby
quelle
0

Obwohl diese Frage ziemlich alt ist und bereits häufig beantwortet wurde, möchte ich nur einen Hinweis zur ordnungsgemäßen Ausnahmebehandlung in C ++ 11 hinzufügen, da ich dies in Diskussionen über Ausnahmen immer wieder vermisse:

Verwenden Sie std::nested_exceptionundstd::throw_with_nested

In StackOverflow wird hier und hier beschrieben , wie Sie eine Rückverfolgung Ihrer Ausnahmen in Ihrem Code erhalten können, ohne dass ein Debugger oder eine umständliche Protokollierung erforderlich ist, indem Sie einfach einen geeigneten Ausnahmebehandler schreiben, der verschachtelte Ausnahmen erneut auslöst.

Da Sie dies mit jeder abgeleiteten Ausnahmeklasse tun können, können Sie einer solchen Rückverfolgung viele Informationen hinzufügen! Sie können sich auch mein MWE auf GitHub ansehen, wo ein Backtrace ungefähr so ​​aussehen würde:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

Sie müssen nicht einmal eine Unterklasse std::runtime_errorerstellen, um beim Auslösen einer Ausnahme zahlreiche Informationen zu erhalten.

Der einzige Vorteil, den ich in Unterklassen sehe (anstatt nur zu verwenden std::runtime_error), ist, dass Ihr Ausnahmebehandler Ihre benutzerdefinierte Ausnahme abfangen und etwas Besonderes tun kann. Beispielsweise:

try
{
  // something that may throw
}
catch( const MyException & ex )
{
  // do something specialized with the
  // additional info inside MyException
}
catch( const std::exception & ex )
{
  std::cerr << ex.what() << std::endl;
}
catch( ... )
{
  std::cerr << "unknown exception!" << std::endl;
}
GPMueller
quelle