Verheerende Ausnahmen

Noch einmal darüber, warum es schlecht ist, Ausnahmen in Destruktoren zu werfen


Viele C ++ - Experten (z. B. Herb Sutter ) lehren uns, dass das Auslösen von Ausnahmen in Destruktoren schlecht ist, da Sie während der Stack-Promotion in den Destruktor gelangen können, wenn bereits eine Ausnahme ausgelöst wird. Wenn in diesem Moment eine andere Ausnahme ausgelöst wird, wird std als Ergebnis aufgerufen :: terminate () . Der C ++ 17-Sprachstandard (im Folgenden verweise ich auf die frei verfügbare Version des Entwurfs N4713 ) zu diesem Thema sagt uns Folgendes:


18.5.1 Die Funktion std :: terminate () [außer.terminate]

1 In einigen Situationen muss die Ausnahmebehandlung für weniger subtile Fehlerbehandlungstechniken abgebrochen werden. [Anmerkung:

Diese Situationen sind:

...

(1.4) wenn die Zerstörung eines Objekts während des Abwickelns des Stapels (18.2) durch Auslösen einer Ausnahme endet, oder

...

- Endnote]

Schauen wir uns ein einfaches Beispiel an:


#include <iostream> class PrintInDestructor { public: ~PrintInDestructor() noexcept { std::cerr << "~PrintInDestructor() invoked\n"; } }; void throw_int_func() { std::cerr << "throw_int_func() invoked\n"; throw 1; } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { std::cerr << "~ThrowInDestructor() invoked\n"; throw_int_func(); } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor bad; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* c) { std::cerr << "Catched const char* exception: " << c << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Ergebnis:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked terminate called after throwing an instance of 'int' Aborted 

Beachten Sie, dass der PrintInDestructor- Destruktor weiterhin aufgerufen wird, d. H. Nach dem Auslösen der zweiten Ausnahme wird die Stapelheraufstufung nicht unterbrochen. Der Standard (derselbe Absatz 18.5.1) zu diesem Thema lautet wie folgt:


2 ... In der Situation, in der kein passender Handler gefunden wird,
Es ist implementierungsdefiniert, ob der Stack abgewickelt wird, bevor std :: terminate () aufgerufen wird. In
die Situation, in der die Suche nach einem Handler (18.3) auf den äußersten Block einer Funktion mit a trifft
Nicht auslösende Ausnahmespezifikation (18.4), es ist implementierungsdefiniert, ob der Stapel abgewickelt wird.
teilweise oder gar nicht abgewickelt, bevor std :: terminate () aufgerufen wird ...

Ich habe dieses Beispiel an mehreren Versionen von GCC (8.2, 7.3) und Clang (6.0, 5.0) getestet, überall dort, wo die Förderung des Stacks fortgesetzt wird. Wenn Sie auf einen Compiler stoßen, bei dem die Implementierung anders definiert ist, schreiben Sie dies bitte in den Kommentaren.


Es sollte auch beachtet werden, dass std :: terminate () nur dann aufgerufen wird, wenn der Stapel abgewickelt wird, wenn eine Ausnahme aus dem Destruktor ausgelöst wird. Wenn sich im Destruktor ein Try / Catch-Block befindet, der die Ausnahme abfängt und nicht weiter auslöst, wird die Heraufstufung des Stapels der externen Ausnahme nicht unterbrochen.


 class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { throw_int_func(); } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor good; std::cerr << "ThrowCatchInDestructor instance created\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Anzeigen


 ThrowCatchInDestructor instance created throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Wie vermeide ich unangenehme Situationen? Theoretisch ist alles einfach: Wirf niemals Ausnahmen in den Destruktor. In der Praxis ist es jedoch nicht so einfach, diese einfache Anforderung schön und elegant zu verwirklichen.


Wenn Sie nicht können, aber wirklich wollen ...


Ich werde sofort bemerken, dass ich nicht versuche, Ausnahmen vom Destruktor zu rechtfertigen, und nach Sutter, Meyers und anderen C ++ - Gurus fordere ich Sie auf, dies niemals zu versuchen (zumindest im neuen Code). In der Praxis kann ein Programmierer jedoch möglicherweise auf Legacy-Code stoßen, der nicht so einfach zu hohen Standards führt. Darüber hinaus können die nachfolgend häufig beschriebenen Techniken während des Debugging-Prozesses nützlich sein.

Zum Beispiel entwickeln wir eine Bibliothek mit einer Wrapper-Klasse, die die Arbeit mit einer bestimmten Ressource kapselt. In Übereinstimmung mit den Prinzipien von RAII greifen wir auf die Ressource im Konstruktor zu und müssen sie im Destruktor freigeben. Was aber, wenn ein Versuch, eine Ressource freizugeben, fehlschlägt? Optionen zur Lösung dieses Problems:


  • Ignorieren Sie den Fehler. Schlecht, weil wir ein Problem verbergen, das andere Teile des Systems betreffen könnte.
  • Schreiben Sie in das Protokoll. Besser als es einfach zu ignorieren, aber immer noch schlecht, weil Unsere Bibliothek weiß nichts über die Protokollierungsrichtlinien, die in dem System angewendet werden, das sie verwendet. Das Standardprotokoll kann nach / dev / null umgeleitet werden, wodurch wiederum kein Fehler angezeigt wird.
  • Nehmen Sie die Freigabe der Ressource in eine separate Funktion, die einen Wert zurückgibt oder eine Ausnahme auslöst, und zwingen Sie den Klassenbenutzer, ihn selbst aufzurufen. Es ist schlecht, weil der Benutzer dies überhaupt vergessen kann und wir ein Ressourcenleck erhalten.
  • Eine Ausnahme auslösen. Gut in gewöhnlichen Fällen, wie Der Klassenbenutzer kann die Ausnahme abfangen und auf standardmäßige Weise Informationen über den Fehler abrufen. Schlecht während der Stapelwerbung, da führt zu std :: terminate () .

Wie kann man verstehen, ob wir gerade dabei sind, den Stack ausnahmsweise zu bewerben oder nicht? In C ++ gibt es dafür eine spezielle Funktion std :: uncaught_exception () . Mit seiner Hilfe können wir in einer normalen Situation sicher eine Ausnahme auslösen oder etwas weniger Richtiges tun, was jedoch nicht dazu führt, dass während der Stapel-Promotion eine Ausnahme ausgelöst wird.


 class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~ThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~ThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor normal; std::cerr << "ThrowInDestructor normal destruction\n"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } try { ThrowInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Ergebnis:


 ThrowInDestructor normal destruction ~ThrowInDestructor() normal case, throwing throw_int_func() invoked ~PrintInDestructor() invoked Catched int exception: 1 ThrowInDestructor stack unwinding ~ThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Beachten Sie, dass die Funktion std :: uncaught_exception () seit C ++ Standard 17 veraltet ist. Um das Beispiel zu kompilieren, muss daher das entsprechende Vorning unterdrückt werden (siehe Repository mit Beispielen aus dem Artikel ).


Das Problem mit dieser Funktion ist, dass sie prüft, ob wir den Stapel ausnahmsweise drehen. Es ist jedoch unmöglich zu verstehen, ob der aktuelle Destruktor während des Prozesses der Stapelheraufstufung aufgerufen wird. Wenn eine Stapelheraufstufung vorliegt, der Destruktor eines Objekts jedoch normal aufgerufen wird, gibt std :: uncaught_exception () weiterhin true zurück .


 class MayThrowInDestructor { public: ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Ergebnis:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Im neuen C ++ 17-Standard wurde die Funktion std :: uncaught_exceptions () eingeführt, um std :: uncaught_exception () zu ersetzen (beachten Sie den Plural), die anstelle eines Booleschen Werts die Anzahl der derzeit aktiven Ausnahmen zurückgibt (hier eine ausführliche Begründung ).


So wird das oben beschriebene Problem mit std :: uncaught_exceptions () gelöst:


 class MayThrowInDestructor { public: MayThrowInDestructor() : exceptions_(std::uncaught_exceptions()) {} ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exceptions() > exceptions_) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: int exceptions_; }; class ThrowCatchInDestructor { public: ~ThrowCatchInDestructor() noexcept(false) { try { MayThrowInDestructor may_throw; } catch (int i) { std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "\n"; } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowCatchInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; throw "BANG!"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } return 0; } 

Ergebnis:


 ThrowInDestructor stack unwinding ~MayThrowInDestructor() normal case, throwing throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG! 

Wenn ich wirklich, wirklich ein paar Ausnahmen auf einmal werfen möchte


std :: uncaught_exceptions () vermeidet das Aufrufen von std :: terminate () , hilft jedoch nicht dabei, mehrere Ausnahmen korrekt zu behandeln. Im Idealfall hätte ich gerne einen Mechanismus, mit dem ich alle ausgelösten Ausnahmen speichern und dann an einem Ort verarbeiten kann.


Ich möchte noch einmal daran erinnern, dass der von mir unten vorgeschlagene Mechanismus nur zur Demonstration des Konzepts dient und nicht für die Verwendung in echtem Industriecode empfohlen wird.

Das Wesentliche der Idee ist, Ausnahmen abzufangen und in einem Container zu speichern und sie dann einzeln abzurufen und zu verarbeiten. Um Ausnahmeobjekte zu speichern, hat C ++ einen speziellen Typ std :: exception_ptr . Die Typstruktur im Standard wird nicht offenbart, es wird jedoch gesagt, dass es sich im Wesentlichen um shared_ptr pro Ausnahmeobjekt handelt.


Wie werden diese Ausnahmen dann verarbeitet? Hierfür gibt es eine Funktion std :: rethrow_exception () , die einen Zeiger std :: exception_ptr verwendet und die entsprechende Ausnahme auslöst . Wir müssen es nur mit dem entsprechenden catch-Abschnitt abfangen und verarbeiten, wonach wir mit dem nächsten Ausnahmeobjekt fortfahren können.


 using exceptions_queue = std::stack<std::exception_ptr>; // Get exceptions queue for current thread exceptions_queue& get_queue() { thread_local exceptions_queue queue_; return queue_; } // Invoke functor and save exception in queue void safe_invoke(std::function<void()> f) noexcept { try { f(); } catch (...) { get_queue().push(std::current_exception()); } } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept { std::cerr << "~ThrowInDestructor() invoked\n"; safe_invoke([]() { throw_int_func(); }); } private: PrintInDestructor member_; }; int main(int, char**) { safe_invoke([]() { ThrowInDestructor bad; throw "BANG!"; }); auto& q = get_queue(); while (!q.empty()) { try { std::exception_ptr ex = q.top(); q.pop(); if (ex != nullptr) { std::rethrow_exception(ex); } } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } catch (const char* s) { std::cerr << "Catched const char* exception: " << s << "\n"; } catch (...) { std::cerr << "Catched unknown exception\n"; } } return 0; } 

Ergebnis:


 ~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked Catched const char* exception: BANG! Catched int exception: 1 

Im obigen Beispiel wird der Stapel zum Speichern von Ausnahmeobjekten verwendet. Die Ausnahmebehandlung wird jedoch gemäß dem FIFO-Prinzip durchgeführt (dh logischerweise ist dies die Warteschlange - die zuerst ausgelöste Ausnahme ist die erste, die verarbeitet wird).


Schlussfolgerungen


Das Auslösen von Ausnahmen in Objektdestruktoren ist wirklich eine schlechte Idee, und in jedem neuen Code empfehle ich dringend, dies nicht zu tun, indem Sie keine Ausnahmedestruktoren deklarieren. Bei der Unterstützung und dem Debuggen von Legacy-Code kann es jedoch erforderlich sein, Ausnahmen, die von Destruktoren ausgelöst werden, auch während der Stack-Heraufstufung, korrekt zu behandeln, und modernes C ++ bietet uns Mechanismen dafür. Ich hoffe, dass die im Artikel vorgestellten Ideen Ihnen auf diesem schwierigen Weg helfen werden.


Referenzen


Repository mit Beispielen aus dem Artikel

Source: https://habr.com/ru/post/de433944/


All Articles