Verwenden von RAII zum Verschachteln von Ausnahmen
-
21-12-2019 - |
Frage
Also der Weg, Ausnahmen in C ++ zu verschachteln mit std::nested_exception
is:
void foo() {
try {
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
catch(...) {
std::throw_with_nested(std::runtime_error("foo failed"));
}
}
Diese Technik verwendet jedoch explizite Try / Catch-Blöcke auf jeder Ebene, auf der Ausnahmen verschachtelt werden sollen, was gelinde gesagt hässlich ist.
RAII, der Jon Kalb expandiert da "Verantwortungsübernahme Initialisierung ist", ist dies eine viel sauberere Methode, um mit Ausnahmen umzugehen, anstatt explizite Try / Catch-Blöcke zu verwenden.Bei RAII werden explizite Try / Catch-Blöcke größtenteils nur verwendet, um letztendlich eine Ausnahme zu behandeln, z.um dem Benutzer eine Fehlermeldung anzuzeigen.
Wenn ich mir den obigen Code anschaue, scheint es mir, dass die Eingabe foo()
kann als eine Verantwortung angesehen werden, Ausnahmen als zu melden std::runtime_error("foo failed")
und verschachteln Sie die Details in einer nested_exception .Wenn wir RAII verwenden können, um diese Verantwortung zu übernehmen, sieht der Code viel sauberer aus:
void foo() {
Throw_with_nested on_error("foo failed");
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
Gibt es hier eine Möglichkeit, die RAII-Syntax zu verwenden, um explizite Try / Catch-Blöcke zu ersetzen?
Dazu benötigen wir einen Typ, der beim Aufruf seines Destruktors prüft, ob der Destruktoraufruf auf eine Ausnahme zurückzuführen ist, diese Ausnahme in diesem Fall verschachtelt und die neue, verschachtelte Ausnahme auslöst, sodass das Abwickeln normal fortgesetzt wird.Das könnte so aussehen:
struct Throw_with_nested {
const char *msg;
Throw_with_nested(const char *error_message) : msg(error_message) {}
~Throw_with_nested() {
if (std::uncaught_exception()) {
std::throw_with_nested(std::runtime_error(msg));
}
}
};
Jedoch std::throw_with_nested()
erfordert, dass eine 'aktuell behandelte Ausnahme' aktiv ist, was bedeutet, dass sie nur im Kontext eines catch-Blocks funktioniert.Also würden wir so etwas brauchen wie:
~Throw_with_nested() {
if (std::uncaught_exception()) {
try {
rethrow_uncaught_exception();
}
catch(...) {
std::throw_with_nested(std::runtime_error(msg));
}
}
}
Leider gibt es meines Wissens nichts Vergleichbares rethrow_uncaught_excpetion()
definiert in C++.
Lösung
In Ermangelung einer Methode zum Abfangen (und Konsumieren) der nicht abgefangenen Ausnahme im Destruktor gibt es keine Möglichkeit, eine verschachtelte oder nicht verschachtelte Ausnahme im Kontext des Destruktors erneut auszulösen, ohne std::terminate
aufgerufen wird (wenn die Ausnahme im Kontext der Ausnahmebehandlung ausgelöst wird).
std::current_exception
(kombiniert mit std::rethrow_exception
) gibt nur einen Zeiger auf eine aktuell behandelte Ausnahme zurück.Dies schließt seine Verwendung in diesem Szenario aus, da die Ausnahme in diesem Fall explizit nicht behandelt wird.
In Anbetracht dessen ist die einzige Antwort aus ästhetischer Sicht zu geben.Try-Blöcke auf Funktionsebene lassen dies etwas weniger hässlich aussehen.(passen sie für ihre stilpräferenz):
void foo() try {
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
}
catch(...) {
std::throw_with_nested(std::runtime_error("foo failed"));
}
Andere Tipps
Mit RAII ist das unmöglich
In Anbetracht der einfachen Regel
Destruktoren dürfen niemals werfen.
mit RAII ist es unmöglich, das zu implementieren, was Sie wollen.Die Regel hat einen einfachen Grund:Wenn ein Destruktor beim Abwickeln des Stapels aufgrund einer Ausnahme im Flug eine Ausnahme auslöst, dann terminate()
wird aufgerufen und Ihre Bewerbung ist tot.
Alternative
In C ++ 11 können Sie mit Lambdas arbeiten, die das Leben etwas erleichtern können.Du kannst schreiben
void foo()
{
giveErrorContextOnFailure( "foo failed", [&]
{
// code that might throw
std::ifstream file("nonexistent.file");
file.exceptions(std::ios_base::failbit);
} );
}
wenn Sie die Funktion implementieren giveErrorContextOnFailure
auf folgende Weise:
template <typename F>
auto giveErrorContextOnFailure( const char * msg, F && f ) -> decltype(f())
{
try { return f(); }
catch { std::throw_with_nested(std::runtime_error(msg)); }
}
Dies hat mehrere Vorteile:
- Sie kapseln ein, wie der Fehler verschachtelt ist.
- Das Ändern der Art und Weise, wie Fehler verschachtelt werden, kann für das gesamte Programm geändert werden, wenn diese Technik strikt programmweit befolgt wird.
- Die Fehlermeldung kann wie in RAII vor den Code geschrieben werden.Diese Technik kann auch für verschachtelte Bereiche verwendet werden.
- Es gibt weniger Code-Wiederholungen:Du musst nicht schreiben
try
,catch
,std::throw_with_nested
undstd::runtime_error
.Dies macht Ihren Code leichter wartbar.Wenn Sie das Verhalten Ihres Programms ändern möchten, müssen Sie Ihren Code nur an einer Stelle ändern. - Der Rückgabetyp wird automatisch abgeleitet.Also wenn deine Funktion
foo()
sollte etwas zurückgeben, dann fügen Sie einfach hinzureturn
vorgiveErrorContextOnFailure
in deiner Funktion foo() .
Im Release-Modus gibt es normalerweise kein Performance-Panel im Vergleich zur Try-Catch-Methode, da Vorlagen standardmäßig inline sind.
Eine weitere interessante Regel zu befolgen:
Nicht verwenden
std::uncaught_exception()
.
Es gibt eine schöne artikel darüber thema von Herb Sutter, das diese Regel perfekt erklärt.Kurz:Wenn Sie eine Funktion haben f()
welches heißt aus einem Destruktor während des Stapelabwickelns sieht so aus
void f()
{
RAII r;
bla();
}
wo der Zerstörer von RAII
sieht aus wie
RAII::~RAII()
{
if ( std::uncaught_exception() )
{
// ...
}
else
{
// ...
}
}
dann wird immer der erste Zweig im Destruktor genommen, da im äußeren Destruktor beim Stapelabwickeln std::uncaught_exception()
gibt immer true zurück, auch innerhalb von Funktionen, die von diesem Destruktor aufgerufen werden, einschließlich des Destruktors von RAII
.