Ihre Frage stellt die Behauptung auf, dass "das Schreiben von ausnahmesicherem Code sehr schwierig ist". Ich werde zuerst Ihre Fragen beantworten und dann die versteckte Frage dahinter beantworten.
Fragen beantworten
Schreiben Sie wirklich ausnahmesicheren Code?
Natürlich tue ich das.
Dies ist der Grund, warum Java für mich als C ++ - Programmierer viel an Attraktivität verloren hat (mangelnde RAII-Semantik), aber ich schweife ab: Dies ist eine C ++ - Frage.
Dies ist in der Tat erforderlich, wenn Sie mit STL- oder Boost-Code arbeiten müssen. Beispielsweise lösen C ++ - Threads ( boost::thread
oder std::thread
) eine Ausnahme aus, um ordnungsgemäß beendet zu werden.
Sind Sie sicher, dass Ihr letzter "produktionsbereiter" Code ausnahmesicher ist?
Können Sie sich überhaupt sicher sein, dass es so ist?
Das Schreiben von ausnahmesicherem Code ist wie das Schreiben von fehlerfreiem Code.
Sie können nicht 100% sicher sein, dass Ihr Code ausnahmesicher ist. Aber dann streben Sie danach, indem Sie bekannte Muster verwenden und bekannte Anti-Muster vermeiden.
Kennen und / oder verwenden Sie Alternativen, die funktionieren?
In C ++ gibt es keine praktikablen Alternativen (dh Sie müssen zu C zurückkehren und C ++ - Bibliotheken sowie externe Überraschungen wie Windows SEH vermeiden).
Ausnahmesicheren Code schreiben
Um einen ausnahmesicheren Code zu schreiben, müssen Sie zuerst wissen, wie hoch die Ausnahmesicherheit für jede Anweisung ist, die Sie schreiben.
Zum Beispiel new
kann a eine Ausnahme auslösen, aber das Zuweisen eines integrierten (z. B. eines int oder eines Zeigers) schlägt nicht fehl. Ein Tausch wird niemals scheitern (schreiben Sie niemals einen Wurf-Tausch), ein std::list::push_back
Dosenwurf ...
Ausnahmegarantie
Das erste, was Sie verstehen müssen, ist, dass Sie in der Lage sein müssen, die Ausnahmegarantie aller Ihrer Funktionen zu bewerten:
- keine : Ihr Code sollte das niemals bieten. Dieser Code leckt alles und bricht bei der ersten ausgelösten Ausnahme zusammen.
- Grundlegend : Dies ist die Garantie, die Sie zumindest anbieten müssen, dh wenn eine Ausnahme ausgelöst wird, gehen keine Ressourcen verloren und alle Objekte sind noch vollständig
- stark : Die Verarbeitung ist entweder erfolgreich oder löst eine Ausnahme aus. Wenn sie jedoch ausgelöst wird, befinden sich die Daten in demselben Zustand, als ob die Verarbeitung überhaupt nicht gestartet worden wäre (dies gibt C ++ eine Transaktionsleistung).
- nothrow / nofail : Die Verarbeitung wird erfolgreich sein.
Beispiel für Code
Der folgende Code scheint korrektes C ++ zu sein, bietet jedoch in Wahrheit die Garantie "keine" und ist daher nicht korrekt:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Ich schreibe meinen gesamten Code unter Berücksichtigung dieser Art von Analyse.
Die niedrigste angebotene Garantie ist einfach, aber dann macht die Reihenfolge jedes Befehls die gesamte Funktion "keine", denn wenn 3. wirft, leckt x.
Das erste, was zu tun wäre, wäre, die Funktion "grundlegend" zu machen, dh x in einen intelligenten Zeiger zu setzen, bis es sicher im Besitz der Liste ist:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Jetzt bietet unser Code eine "grundlegende" Garantie. Es wird nichts auslaufen und alle Objekte befinden sich in einem korrekten Zustand. Aber wir könnten mehr bieten, das heißt die starke Garantie. Hier kann es teuer werden, und deshalb ist nicht der gesamte C ++ - Code stark. Lass es uns versuchen:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Wir haben die Vorgänge neu angeordnet, indem wir zuerst X
den richtigen Wert erstellt und eingestellt haben. Wenn eine Operation fehlschlägt, t
wird sie nicht geändert. Daher können die Operationen 1 bis 3 als "stark" betrachtet werden: Wenn etwas ausgelöst wird, t
wird es nicht geändert und X
leckt nicht, da es dem intelligenten Zeiger gehört.
Dann erstellen wir eine Kopie t2
von t
und bearbeiten diese Kopie von Operation 4 bis 7. Wenn etwas wirft, t2
wird es geändert, aber dann t
ist es immer noch das Original. Wir bieten weiterhin die starke Garantie.
Dann tauschen wir t
und t2
. Swap-Operationen sollten in C ++ nicht angezeigt werden. Hoffen wir also, dass der Swap, für den Sie geschrieben haben, nicht T
angezeigt wird (wenn dies nicht der Fall ist, schreiben Sie ihn neu, damit er nicht angezeigt wird).
Wenn wir also das Ende der Funktion erreichen, war alles erfolgreich (kein Rückgabetyp erforderlich) und t
hat seinen Ausnahmewert. Wenn es fehlschlägt, t
hat es immer noch seinen ursprünglichen Wert.
Das Anbieten der starken Garantie kann sehr kostspielig sein. Versuchen Sie also nicht, die starke Garantie für Ihren gesamten Code anzubieten. Wenn Sie dies jedoch ohne Kosten tun können (und C ++ - Inlining und andere Optimierungen können den gesamten oben genannten Code kostenlos machen). , dann tu es. Der Funktionsbenutzer wird es Ihnen danken.
Fazit
Es ist eine Gewohnheit, ausnahmesicheren Code zu schreiben. Sie müssen die Garantie bewerten, die von jeder Anweisung angeboten wird, die Sie verwenden, und dann müssen Sie die Garantie bewerten, die von einer Liste von Anweisungen angeboten wird.
Natürlich wird der C ++ - Compiler die Garantie nicht sichern (in meinem Code biete ich die Garantie als @ warning doxygen-Tag an), was ein bisschen traurig ist, aber es sollte Sie nicht davon abhalten, ausnahmesicheren Code zu schreiben.
Normaler Fehler gegen Fehler
Wie kann ein Programmierer garantieren, dass eine No-Fail-Funktion immer erfolgreich ist? Immerhin könnte die Funktion einen Fehler haben.
Das ist wahr. Die Ausnahmegarantien sollen durch fehlerfreien Code angeboten werden. In jeder Sprache setzt der Aufruf einer Funktion jedoch voraus, dass die Funktion fehlerfrei ist. Kein vernünftiger Code schützt sich vor der Möglichkeit eines Fehlers. Schreiben Sie den Code so gut Sie können, und bieten Sie dann die Garantie mit der Annahme, dass er fehlerfrei ist. Und wenn es einen Fehler gibt, korrigieren Sie ihn.
Ausnahmen sind außergewöhnliche Verarbeitungsfehler, keine Codefehler.
Letzte Worte
Die Frage ist nun "Lohnt sich das?".
Natürlich ist es das. Eine "nothrow / no-fail" -Funktion zu haben, die weiß, dass die Funktion nicht fehlschlägt, ist ein großer Segen. Das Gleiche gilt für eine "starke" Funktion, mit der Sie Code mit Transaktionssemantik wie Datenbanken mit Commit / Rollback-Funktionen schreiben können, wobei das Commit die normale Ausführung des Codes ist und Ausnahmen das Rollback auslösen.
Dann ist das "Basic" die geringste Garantie, die Sie anbieten sollten. C ++ ist dort eine sehr starke Sprache mit ihren Bereichen, die es Ihnen ermöglicht, Ressourcenlecks zu vermeiden (etwas, das ein Garbage Collector nur schwer für die Datenbank-, Verbindungs- oder Dateihandles anbieten kann).
Also, soweit ich es sehe, es ist es wert.
Edit 2010-01-29: Über nicht werfende Swap
nobar hat einen Kommentar abgegeben, der meiner Meinung nach sehr relevant ist, da er Teil von "Wie schreibt man ausnahmesicheren Code?" ist:
- [ich] Ein Tausch wird niemals scheitern (schreibe nicht einmal einen Wurf-Tausch)
- [nobar] Dies ist eine gute Empfehlung für benutzerdefinierte
swap()
Funktionen. Es sollte jedoch beachtet werden, dass std::swap()
dies aufgrund der intern verwendeten Vorgänge fehlschlagen kann
Die Standardeinstellung erstellt std::swap
Kopien und Zuweisungen, die für einige Objekte ausgelöst werden können. Daher könnte der Standard-Swap entweder für Ihre Klassen oder sogar für STL-Klassen verwendet werden. Soweit die C ++ Standard betrifft, für die Swap - Operation vector
, deque
und list
nicht werfen, während es für könnte , map
wenn der Vergleich Funktor auf Kopie Bau werfen (siehe Die Programmiersprache C ++, Special Edition, Anhang E, E.4.3 .Swap ).
Bei der Visual C ++ 2008-Implementierung des Vektortauschs wird der Vektortausch nicht ausgelöst, wenn die beiden Vektoren denselben Zuweiser haben (dh im Normalfall), sondern Kopien erstellen, wenn sie unterschiedliche Zuweiser haben. Und so gehe ich davon aus, dass es in diesem letzten Fall werfen könnte.
Der ursprüngliche Text lautet also weiterhin: Schreiben Sie niemals einen Wurf-Tausch, aber der Kommentar von Nobar muss beachtet werden: Stellen Sie sicher, dass die Objekte, die Sie tauschen, einen nicht-Wurf-Tausch haben.
Edit 2011-11-06: Interessanter Artikel
Dave Abrahams , der uns die grundlegenden / starken / nicht garantierten Garantien gab , beschrieb in einem Artikel seine Erfahrungen mit der Sicherheit der STL-Ausnahme:
http://www.boost.org/community/exception_safety.html
Schauen Sie sich den 7. Punkt an (automatisierte Tests für Ausnahmesicherheit), an dem er sich auf automatisierte Komponententests stützt, um sicherzustellen, dass jeder Fall getestet wird. Ich denke, dieser Teil ist eine ausgezeichnete Antwort auf die Frage des Autors " Kannst du dir überhaupt sicher sein, dass es so ist? ".
Edit 2013-05-31: Kommentar von dionadar
t.integer += 1;
ist ohne die Garantie, dass kein Überlauf auftritt, NICHT ausnahmesicher und kann tatsächlich UB technisch aufrufen! (Der signierte Überlauf ist UB: C ++ 11 5/4. "Wenn das Ergebnis während der Auswertung eines Ausdrucks nicht mathematisch definiert ist oder nicht im Bereich der darstellbaren Werte für seinen Typ liegt, ist das Verhalten undefiniert.") Beachten Sie, dass das Vorzeichen nicht angegeben ist Ganzzahlen laufen nicht über, sondern führen ihre Berechnungen in einer Äquivalenzklasse Modulo 2 ^ # Bits durch.
Dionadar bezieht sich auf die folgende Zeile, die tatsächlich ein undefiniertes Verhalten aufweist.
t.integer += 1 ; // 1. nothrow/nofail
Die Lösung besteht darin, std::numeric_limits<T>::max()
vor dem Hinzufügen zu überprüfen, ob die Ganzzahl bereits ihren Maximalwert erreicht hat (using ).
Mein Fehler würde in den Abschnitt "Normaler Fehler vs. Fehler" gehen, dh ein Fehler. Dies macht die Argumentation nicht ungültig und bedeutet nicht, dass der ausnahmesichere Code nutzlos ist, weil er nicht zu erreichen ist. Sie können sich nicht vor dem Ausschalten des Computers, Compiler-Fehlern oder sogar Ihren Fehlern oder anderen Fehlern schützen. Sie können keine Perfektion erreichen, aber Sie können versuchen, so nah wie möglich zu kommen.
Ich habe den Code unter Berücksichtigung von Dionadars Kommentar korrigiert.