In den häufig gestellten Fragen zur isocpp.org-Ausnahme heißt es
Verwenden Sie throw nicht, um einen Codierungsfehler bei der Verwendung einer Funktion anzuzeigen. Verwenden Sie assert oder einen anderen Mechanismus, um den Prozess entweder in einen Debugger zu senden oder um den Prozess zum Absturz zu bringen und den Absturzspeicherauszug für den Entwickler zum Debuggen zu sammeln.
Andererseits definiert die Standardbibliothek std :: logic_error und alle Ableitungen, die mir so vorkommen, als würden sie neben anderen Dingen auch Programmierfehler behandeln. Ist die Übergabe eines leeren Strings an std :: stof (wirft invalid_argument) kein Programmierfehler? Handelt es sich bei der Übergabe einer Zeichenfolge, die andere Zeichen als '1' / '0' enthält, an std :: bitset nicht um einen Programmierfehler? Ist der Aufruf von std :: bitset :: set mit einem ungültigen Index (wird out_of_range auslösen) kein Programmierfehler? Wenn dies nicht der Fall ist, auf was für einen Programmierfehler würde man dann testen? Der auf std :: bitset-Strings basierende Konstruktor existiert nur seit C ++ 11 und sollte daher unter Berücksichtigung der Ausnahmebedingungen entwickelt worden sein. Andererseits haben mir die Leute gesagt, dass logic_error grundsätzlich überhaupt nicht verwendet werden sollte.
Eine andere Regel, die häufig mit Ausnahmen einhergeht, lautet "Ausnahmen nur in Ausnahmefällen verwenden". Aber wie soll eine Bibliotheksfunktion wissen, welche Umstände außergewöhnlich sind? Bei einigen Programmen kann es außergewöhnlich sein, dass eine Datei nicht geöffnet werden kann. Für andere ist es möglicherweise keine Ausnahme, wenn sie keinen Speicher zuweisen können. Dazwischen liegen Hunderte von Fällen. Sie können keinen Socket erstellen? Sie können keine Verbindung herstellen oder Daten in einen Socket oder eine Datei schreiben? Eingaben können nicht analysiert werden? Könnte außergewöhnlich sein, vielleicht auch nicht. Die Funktion selbst kann definitiv nicht allgemein wissen, sie hat keine Ahnung, in welchem Kontext sie aufgerufen wird.
Wie soll ich also entscheiden, ob ich Ausnahmen für eine bestimmte Funktion verwenden soll oder nicht? Es scheint mir, dass der einzig wirklich konsequente Weg darin besteht, sie für jede und jede Fehlerbehandlung oder für nichts zu verwenden. Und wenn ich die Standardbibliothek verwende, wurde diese Wahl für mich getroffen.
quelle
Antworten:
Erstens fühle ich mich verpflichtet, darauf hinzuweisen, dass
std::exception
und seine Kinder vor langer Zeit entworfen wurden. Es gibt eine Reihe von Teilen, die wahrscheinlich (mit ziemlicher Sicherheit) anders wären, wenn sie heute entworfen würden.Verstehen Sie mich nicht falsch: Es gibt Teile des Designs, die ziemlich gut ausgearbeitet haben, und es gibt ziemlich gute Beispiele für das Entwerfen einer Ausnahmehierarchie für C ++ (z. B. die Tatsache, dass sie im Gegensatz zu den meisten anderen Klassen a gemeinsam haben gemeinsame Wurzel).
Speziell betrachtet
logic_error
haben wir ein Rätsel. Auf der einen Seite, wenn Sie eine vernünftige Wahl in der Sache haben, ist der Rat, den Sie zitiert haben, richtig: Es ist im Allgemeinen am besten, so schnell und geräuschvoll wie möglich zu versagen, damit Fehler behoben werden können.Ob gut oder schlecht, es ist schwierig, die Standardbibliothek so zu definieren, wie Sie es allgemein tun sollten. Wenn es diese definiert, um das Programm zu verlassen (z. B.
abort()
bei falscher Eingabe aufzurufen ), ist dies immer der Fall - und es gibt tatsächlich einige Umstände, unter denen dies wahrscheinlich nicht richtig ist Zumindest im implementierten Code.Dies gilt für Code mit (zumindest weichen) Echtzeitanforderungen und minimaler Strafe für eine falsche Ausgabe. Betrachten Sie beispielsweise ein Chat-Programm. Wenn es darum geht, einige Sprachdaten zu dekodieren und falsche Eingaben zu erhalten, ist es wahrscheinlich, dass ein Benutzer mit einer Millisekunde statischer Aufladung in der Ausgabe viel glücklicher ist als mit einem Programm, das nur vollständig heruntergefahren wird. Ebenso kann es bei der Videowiedergabe akzeptabler sein, für ein oder zwei Bilder die falschen Werte für einige Pixel zu erzeugen, als das Programm zu beenden, da der Eingabestream beschädigt wurde.
Was die Verwendung von Ausnahmen zur Meldung bestimmter Fehlertypen betrifft: Sie haben Recht - derselbe Vorgang kann je nach Verwendungszweck als Ausnahme eingestuft werden oder nicht.
Andererseits liegen Sie auch falsch - die Verwendung der Standardbibliothek erzwingt Ihnen diese Entscheidung (nicht unbedingt). Wenn Sie eine Datei öffnen, verwenden Sie normalerweise einen iostream. Iostreams sind nicht gerade das neueste und beste Design, aber in diesem Fall stimmt es: Sie können einen Fehlermodus festlegen, damit Sie steuern können, ob beim Öffnen einer Datei eine Ausnahme ausgelöst wird oder nicht. Wenn Sie also eine Datei haben, die für Ihre Anwendung wirklich notwendig ist , und wenn Sie diese nicht öffnen, müssen Sie ernsthafte Abhilfemaßnahmen ergreifen, und es kann eine Ausnahme ausgelöst werden, wenn diese Datei nicht geöffnet werden kann. Bei den meisten Dateien, die Sie öffnen möchten, schlagen diese fehl, wenn sie nicht vorhanden sind oder nicht darauf zugegriffen werden kann (dies ist die Standardeinstellung).
Wie Sie sich entscheiden: Ich glaube nicht, dass es eine einfache Antwort gibt. Ob gut oder schlecht, "außergewöhnliche Umstände" sind nicht immer leicht zu messen. Zwar gibt es Fälle, die leicht zu entscheiden sind und [un] außergewöhnlich sein müssen, aber es gibt (und wird wahrscheinlich immer) Fälle, in denen Fragen offen sind oder die Kenntnis des Kontexts erfordern, der außerhalb des Bereichs der jeweiligen Funktion liegt. In solchen Fällen kann es sich zumindest lohnen, ein Design in Betracht zu ziehen, das in etwa diesem Teil von iostreams ähnelt. Hier kann der Benutzer entscheiden, ob ein Fehler zu einer Ausnahmebedingung führt oder nicht. Alternativ ist es durchaus möglich, zwei separate Sätze von Funktionen (oder Klassen usw.) zu haben, von denen einer Ausnahmen auslöst, um einen Fehler anzuzeigen, während der andere andere Mittel verwendet. Wenn Sie diesen Weg gehen,
quelle
Sie werden das vielleicht nicht glauben, aber verschiedene C ++ - Codierer stimmen dem nicht zu. Das ist der Grund, warum die FAQ eines besagt, aber die Standardbibliothek widerspricht.
Die FAQ befürwortet einen Absturz, da dies einfacher zu debuggen ist. Wenn Sie abstürzen und einen Core-Dump erhalten, haben Sie den genauen Status Ihrer Anwendung. Wenn Sie eine Ausnahme auslösen, verlieren Sie einen Großteil dieses Zustands.
Die Standardbibliothek geht von der Theorie aus, dass es wichtiger als die Debug-Fähigkeit ist, dem Codierer die Möglichkeit zu geben, den Fehler abzufangen und möglicherweise zu behandeln.
Die Idee hier ist, dass, wenn Ihre Funktion nicht weiß, ob die Situation außergewöhnlich ist oder nicht, sie keine Ausnahme auslösen sollte. Es sollte über einen anderen Mechanismus einen Fehlerstatus zurückgeben. Sobald es einen Punkt im Programm erreicht, an dem es weiß, dass der Status außergewöhnlich ist, sollte es die Ausnahme auslösen.
Das hat aber ein eigenes Problem. Wenn ein Fehlerstatus von einer Funktion zurückgegeben wird, können Sie sich möglicherweise nicht daran erinnern, ihn zu überprüfen, und der Fehler wird unbemerkt weitergegeben. Dies führt dazu, dass einige Leute die Ausnahmeregeln aufgeben, um Ausnahmen für jede Art von Fehlerstatus auszulösen.
Insgesamt ist der entscheidende Punkt, dass verschiedene Leute unterschiedliche Vorstellungen haben, wann Ausnahmen zu werfen sind. Sie werden keine einzige zusammenhängende Idee finden. Auch wenn manche Willenskünstler dogmatisch behaupten, dass dies oder das der richtige Weg ist, mit Ausnahmen umzugehen, gibt es keine vereinbarte Theorie.
Sie können Ausnahmen auslösen:
und jemanden im Internet finden, der Ihnen zustimmt. Sie müssen den Stil übernehmen, der für Sie funktioniert.
quelle
Viele andere gute Antworten wurden geschrieben, ich möchte nur einen kurzen Punkt hinzufügen.
Die traditionelle Antwort vergleicht, besonders wenn die ISO C ++ - FAQ geschrieben wurde, hauptsächlich "C ++ - Ausnahme" mit "C-artigem Rückkehrcode". Eine dritte Option, "eine Art zusammengesetzten Wert zurückgeben, z. B. a
struct
oderunion
oder heutzutageboost::variant
oder der (vorgeschlagene)std::expected
, wird nicht berücksichtigt.Vor C ++ 11 war die Option "Composite-Typ zurückgeben" normalerweise sehr schwach. Weil es keine Verschiebungssemantik gab, war das Kopieren von Dingen in und aus einer Struktur möglicherweise sehr teuer. Zu diesem Zeitpunkt war es in der Sprache äußerst wichtig, Ihren Code in Richtung RVO zu formatieren , um die bestmögliche Leistung zu erzielen. Ausnahmen waren wie eine einfache Möglichkeit, einen zusammengesetzten Typ effektiv zurückzugeben, wenn dies sonst ziemlich schwierig wäre.
IMO, nach C ++ 11 sollte diese Option "return a discriminated union", ähnlich der
Result<T, E>
in Rust heute verwendeten Redewendung , im C ++ - Code häufiger bevorzugt werden. Manchmal ist es wirklich eine einfachere und bequemere Art, Fehler anzuzeigen. Mit Ausnahmen gibt es immer diese Möglichkeit, dass Funktionen, die zuvor nicht geworfen wurden, plötzlich nach einem Refactor geworfen werden können, und Programmierer dokumentieren solche Dinge nicht immer so gut. Wenn der Fehler als Teil des Rückgabewerts in einer diskriminierten Union angegeben wird, wird die Wahrscheinlichkeit, dass der Programmierer den Fehlercode einfach ignoriert, erheblich verringert. Dies ist die übliche Kritik an der Fehlerbehandlung im C-Stil.Funktioniert normalerweise
Result<T, E>
wie Boost optional. Sie können mit testen,operator bool
ob es sich um einen Wert oder einen Fehler handelt. Und dann verwenden Sie say,operator *
um auf den Wert oder eine andere "get" -Funktion zuzugreifen. Normalerweise ist dieser Zugriff aus Gründen der Geschwindigkeit deaktiviert. Sie können jedoch festlegen, dass bei einem Debugbuild der Zugriff überprüft wird und eine Zusicherung sicherstellt, dass tatsächlich ein Wert und kein Fehler vorliegt. Auf diese Weise erhält jeder, der nicht richtig nach Fehlern sucht, eine harte Behauptung und kein heimtückischeres Problem.Ein zusätzlicher Vorteil ist, dass im Gegensatz zu Ausnahmen, bei denen der Stapel nur um eine willkürliche Strecke nach oben geschleudert wird, wenn eine Funktion einen Fehler meldet, bei dem dies zuvor nicht der Fall war, Sie ihn erst kompilieren können, wenn Code wird geändert, um damit umzugehen. Dies macht die Probleme lauter - das traditionelle Problem der "nicht erfassten Ausnahme" wird eher zu einem Fehler bei der Kompilierung als zu einem Laufzeitfehler.
Ich bin ein großer Fan dieses Stils geworden. Normalerweise verwende ich heutzutage entweder diese oder Ausnahmen. Aber ich versuche, die Ausnahmen auf größere Probleme zu beschränken. Bei so etwas wie einem Analysefehler versuche ich
expected<T>
zum Beispiel zurückzukehren. Dinge wiestd::stoi
undboost::lexical_cast
die eine C ++ - Ausnahme auslösen, wenn ein relativ kleines Problem auftritt: "Zeichenfolge konnte nicht in Zahl konvertiert werden", schmecken mir heutzutage sehr schlecht.quelle
std::expected
ist noch ein nicht angenommener vorschlag, oder?exception_ptr
, oder möchten Sie nur einen Strukturtyp oder etwas anderes verwenden? so wie das.[[nodiscard]] attribute
Methode ist für diese Fehlerbehandlung hilfreich, da sie sicherstellt, dass Sie das Fehlerergebnis nicht versehentlich einfach ignorieren.except_ptr
) intern eine Ausnahme auslösen musste. Persönlich denke ich, dass ein solches Tool völlig unabhängig von der Ausführung funktionieren sollte. Nur eine Bemerkung.Dies ist ein sehr subjektives Thema, da es Teil des Designs ist. Und weil Design im Grunde genommen Kunst ist, diskutiere ich diese Dinge lieber als zu diskutieren (ich sage nicht, dass Sie debattieren).
Für mich gibt es zwei Arten von Ausnahmefällen: solche, die sich mit Ressourcen befassen, und solche, die sich mit kritischen Vorgängen befassen. Was als kritisch angesehen werden kann, hängt vom jeweiligen Problem und in vielen Fällen vom Standpunkt des Programmierers ab.
Das Versäumnis, Ressourcen zu erwerben, ist ein Hauptkandidat für das Werfen von Ausnahmen. Die Ressource kann Arbeitsspeicher, Datei, Netzwerkverbindung oder alles andere sein, je nach Problem und Plattform. Ist die Nichtfreigabe einer Ressource eine Ausnahme? Nun, das kommt wieder darauf an. Ich habe noch nichts getan, bei dem die Freigabe des Speichers fehlgeschlagen ist, daher bin ich mir bei diesem Szenario nicht sicher. Das Löschen von Dateien als Teil der Ressourcenfreigabe kann jedoch fehlschlagen und ist für mich fehlgeschlagen. Dieser Fehler hängt normalerweise mit einem anderen Prozess zusammen, der ihn in einer Mehrprozessanwendung geöffnet gelassen hat. Ich vermute, dass andere Ressourcen während der Veröffentlichung fehlschlagen könnten, wie dies bei einer Datei der Fall sein könnte, und es ist normalerweise ein Konstruktionsfehler, der dieses Problem verursacht. Es wäre also besser, ihn zu beheben, als eine Ausnahme zu werfen.
Dann kommt die Aktualisierung der Ressourcen. Dieser Punkt hängt für mich zumindest eng mit dem Aspekt der kritischen Operationen der Anwendung zusammen. Stellen Sie sich eine
Employee
Klasse mit einer Funktion vorUpdateDetails(std::string&)
, die die Details basierend auf der angegebenen kommagetrennten Zeichenfolge ändert. Ähnlich wie bei der Freigabe von Speicherfehlern fällt es mir schwer, mir vorzustellen, dass die Zuweisung von Membervariablenwerten fehlschlägt, da ich in solchen Bereichen, in denen dies auftreten könnte, keine Erfahrung habe. EsUpdateDetailsAndUpdateFile(std::string&)
ist jedoch zu erwarten , dass eine Funktion wie die, die der Name angibt, fehlschlägt. Das nenne ich kritische Operation.Nun müssen Sie prüfen, ob die so genannte kritische Operation die Auslösung einer Ausnahme rechtfertigt. Ich meine, geschieht die Aktualisierung der Datei am Ende, wie im Destruktor, oder ist es einfach ein paranoider Aufruf, der nach jeder Aktualisierung durchgeführt wird? Gibt es einen Fallback-Mechanismus, der regelmäßig ungeschriebene Objekte schreibt? Was ich sage, ist, dass Sie die Kritikalität der Operation bewerten müssen.
Offensichtlich gibt es viele kritische Vorgänge, die nicht an Ressourcen gebunden sind. Wenn die
UpdateDetails()
Daten falsch sind, werden die Details nicht aktualisiert und der Fehler muss gemeldet werden, sodass Sie hier eine Ausnahme auslösen würden. Aber stell dir eine Funktion vor wieGiveRaise()
. Wenn der besagte Mitarbeiter das Glück hat, einen spitzen Chef zu haben und keine Erhöhung zu erhalten (programmtechnisch gesehen, verhindert der Wert einiger Variablen dies), ist die Funktion im Wesentlichen fehlgeschlagen. Würden Sie hier eine Ausnahme machen? Was ich sage, ist, dass Sie die Notwendigkeit einer Ausnahme bewerten müssen.Für mich ist Konsistenz in Bezug auf meinen Designansatz wichtiger als die Benutzerfreundlichkeit meiner Klassen. Was ich damit meine ist, ich denke nicht, dass alle Get-Funktionen dies tun müssen und alle Update-Funktionen dies tun müssen, sondern ob eine bestimmte Funktion eine bestimmte Idee in meinem Ansatz anspricht. Auf den ersten Blick könnten die Klassen wie zufällig aussehen, aber wenn die Benutzer (meistens Kollegen aus anderen Teams) darüber schimpfen oder danach fragen, erkläre ich es ihnen und sie scheinen zufrieden zu sein.
Ich sehe viele Leute, die im Grunde genommen Rückgabewerte durch Ausnahmen ersetzen, weil sie C ++ und nicht C verwenden, und dass es eine "nette Trennung der Fehlerbehandlung" usw. gibt, und fordere mich auf, das "Mischen" von Sprachen usw. zu stoppen, von denen ich normalerweise Abstand halte solche Menschen.
quelle
Erstens, wie andere bereits festgestellt haben, sind die Dinge in C ++ nicht so eindeutig, hauptsächlich deshalb, weil die Anforderungen und Einschränkungen in C ++ etwas vielfältiger sind als in anderen Sprachen, insb. C # und Java, die "ähnliche" Ausnahmefehler aufweisen.
Ich werde das Beispiel std :: stof belichten:
Der Grundvertrag , wie ich es sehe, diese Funktion ist , dass sie versucht es das Argument mit einem Schwimmer zu konvertieren, und jeder Ausfall , dies zu tun wird durch eine Ausnahme gemeldet. Beide möglichen Ausnahmen ergeben sich,
logic_error
jedoch nicht im Sinne eines Programmiererfehlers, sondern im Sinne von "Die Eingabe kann niemals in einen Gleitkommawert umgewandelt werden".Hier kann man sagen, dass a verwendet
logic_error
wird, um anzuzeigen, dass es angesichts dieser (Laufzeit-) Eingabe immer ein Fehler ist, es zu konvertieren - aber es ist die Aufgabe der Funktion, dies zu bestimmen und Ihnen (über eine Ausnahme) mitzuteilen.Randnotiz: In dieser Ansicht
runtime_error
könnte a als etwas angesehen werden, das bei gleicher Eingabe für eine Funktion theoretisch für verschiedene Läufe erfolgreich sein könnte. (zB eine Dateioperation, DB-Zugriff, etc.)Weiterer Side Hinweis: Die C ++ Bibliothek Regex wählte aus seinem Fehler ableiten ,
runtime_error
obwohl es Fälle gibt , in denen es könnte das gleiche wie hier (ungültiger RegexMuster) klassifiziert werden.Dies zeigt nur, meiner Meinung nach, dass Gruppierung in
logic_
oderruntime_
Fehler in C ++ ziemlich unscharf ist und im allgemeinen Fall nicht wirklich viel hilft (*) - wenn Sie bestimmte Fehler behandeln müssen, müssen Sie wahrscheinlich weniger als die beiden abfangen.(*): Das ist nicht zu sagen , dass ein einzelnes Stück Code nicht konsistent sein sollte, aber ob Sie werfen
runtime_
oderlogic_
odercustom_
Somethings ist wirklich nicht so wichtig, denke ich.Kommentar zu beiden
stof
undbitset
:Beide Funktionen nehmen Strings als Argument und in beiden Fällen ist es:
Diese Aussage hat meiner Meinung nach zwei Wurzeln:
Leistung : Wenn eine Funktion in einem kritischen Pfad aufgerufen wird und der "Ausnahmefall" kein Ausnahmefall ist, dh bei einer signifikanten Anzahl von Durchläufen eine Ausnahme ausgelöst wird, ist es nicht sinnvoll, jedes Mal für die Ausnahmeabwicklungsmaschine zu zahlen und möglicherweise zu langsam.
Lokalität der Fehlerbehandlung : Wenn eine Funktion aufgerufen wird und die Ausnahme sofort abgefangen und verarbeitet wird, ist es wenig sinnvoll, eine Ausnahme auszulösen, da die Fehlerbehandlung bei der ausführlicher ist
catch
als bei einerif
.Beispiel:
Hier kommen Funktionen wie
TryParse
vs.Parse
ins Spiel: Eine Version, bei der der lokale Code erwartet, dass die analysierte Zeichenfolge gültig ist, eine Version, bei der der lokale Code davon ausgeht, dass tatsächlich erwartet wird (dh nicht außergewöhnlich), dass das Analysieren fehlschlägt.Ist in der Tat
stof
nur (definiert als) ein Wrapperstrtof
, wenn Sie also keine Ausnahmen wollen, verwenden Sie diesen.IMHO, Sie haben zwei Fälle:
"Bibliothek" -ähnliche Funktion (wird häufig in verschiedenen Kontexten wiederverwendet): Sie können sich im Grunde nicht entscheiden. Geben Sie möglicherweise beide Versionen an, möglicherweise eine, die einen Fehler meldet, und eine Wrappper-Version, die den zurückgegebenen Fehler in eine Ausnahme umwandelt.
"Application" -Funktion (spezifisch für einen Blob von Anwendungscode, kann teilweise wiederverwendet werden, wird jedoch durch den Fehlerbehandlungsstil der Apps usw. eingeschränkt): Hier sollte es häufig ziemlich eindeutig sein. Wenn die Codepfade, die die Funktionen aufrufen, Ausnahmen auf vernünftige und nützliche Weise behandeln, verwenden Sie Ausnahmen, um einen Fehler (siehe unten) zu melden . Wenn der Anwendungscode für einen Fehlerrückgabestil leichter gelesen und geschrieben werden kann, verwenden Sie dies auf jeden Fall.
Natürlich wird es dazwischen Plätze geben - benutze einfach was nötig ist und erinnere dich an YAGNI.
Zuletzt denke ich, ich sollte zur FAQ-Erklärung zurückkehren,
Ich abonniere dies für alle Fehler, die eindeutig darauf hinweisen, dass etwas stark durcheinander ist oder der aufrufende Code eindeutig nicht wusste, was er tat.
Aber wenn dies angebracht ist, ist es oft sehr anwendungsspezifisch, siehe oben Bibliotheksdomäne vs. Anwendungsdomäne.
Dies fällt zurück auf die Frage, ob und wie zu validieren Aufruf Voraussetzungen , aber ich werde nicht in die, Antwort schon zu lange :-)
quelle