Schnitstellen in C++?
-
Yamakuzure schrieb:
Da das Verhalten bei einem Bruch der
noexcept-Spezifizierung allerdings doch weiter zur Laufzeit stattfindet, bleibt abzuwarten, ob uns Programmierern am Ende Möglichkeiten bleiben, darauf zu reagieren, oder ob kein Weg amterminate()vorbeiführt. (Mir schwant aber, dass es außer dem Setzen eines alternativen terminate-Handlers nichts mehr geben wird.)Du denkst falsch herum.
Aber erstmal: throw() muss unwind Code generieren auch wenn die Funktion garnicht werfen kann. Unwind Code ist teuer. noexcept braucht keinen Unwind Code.
noexcept ist ausserdem ein assert. Es ist kein if().
noexcept sagt: diese Funktion wirft nicht. Das ist das selbe wie wenn manvoid strcpy(char* trg, const char* src) { assert(trg); assert(src); while(*trg++=*src++) ; }sagt. Man will keine extremen Tests und dergleichen machen, sondern man faengt nur logikfehler ab die sich zur Testphase noch im Programm befinden.
Dass throw() dir defakto auch nicht mehr bringt lassen wir mal weg, das wird sonst zu kompliziert. Wenn also eine Exception eine noexcept Funktion verlaesst, dann ist das ein Logikfehler -> invarianten sind nicht korrekt. Du musst sowieso terminaten.
Das schoene an noexcept ist, dass es den Code schneller macht. Ein throw() macht ihn langsamer, ein noexcept macht ihn im idealfall schneller - meistens wohl aber gleichschnell wie eine Funktion ohne Spezifikation.
Deshalb ist noexcept soviel besser als throw(). Es geht einen anderen Weg: denke an assert, __assume, restrict,... noexcept gehoert zu dieser Gruppe.
Warum ein throw() sinnlos ist erklaere ich auch nochmal kurz:
Der unexcepted Handler hat keinen Kontext, du weisst also nicht was du werfen darfst. Sprich er bringt dir als Exception Translator nichts. Weiters hast du auch den Kontext des Fehlers nicht, du kannst also auf nichts reagieren. Alles was du machen kannst ist ein Shutdown der Anwendung (alles andere waere Wahnsinn). Du kannst ja nichtmal das Subsystem identifizieren dass den Fehler produziert hat. Du weisst nur: irgendwo sind Invarianten verletzt, ich kann meinen Daten nicht mehr trauen.throw() hat also Gegenueber noexcept hier keinen Vorteil. Denn so oder so, man ruft terminate() auf.
Nun mag throw() vielleicht ganz gut klingen solange es kein noexcept gibt. Aber es fungiert nur wie ein assert zur Laufzeit. Das kann ich besser haben mit StackGuards und uncaught_exception. Dann hat man naemlich ploetzlich den Kontext wieder.
Sprich: wenn ich Angst habe dass irgendwo eine exception fliegt wo keine fliegen sollte, dann ist throw() ein tragbares mittel. Aber: throw() ist Teil des Interfaces und das sollte ein assert eigentlich nicht sein. Prinzipiell bringt throw() also einige Nachteile gegenueber einem Stackguard mit sich - aber keinen Vorteil.
tl;dr: exception spezifikationen in C++ sind ein NoGo! noexcept dagegen macht Sinn.
zu:
void ichWerfNicht () noexcept { /* tue irgendwas absolut sicheres */ } void foo() { /* tue irgendwas absolut sicheres */ ichWerfNicht(); /* tue irgendwas absolut sicheres */ } /* irgendwo anders: */ bool wasBinIch = noexcept(foo()); // Muesste true ergeben ?!?Das ergibt false. Denn foo darf alles werfen was es will.
The result of the noexcept operator is false if in a potentially-evaluated context the expression would contain [..] a potentially evaluated call to a function [...] that does not have a non-throwing exception-specification
-
Yamakuzure schrieb:
Also: Wenn ich die Ausnahmespezifikation
throw()verwende, darf die Methode nicht werfen, oder es wirdunexpected()aufgerufen. Dieses ruft seinerseits, solange man den Handler nicht ändert,terminate()auf. Wenn ich die Ausnahmespezifikationnoexceptbzwnoexcept(true)verwende, darf die Methode nicht werfen, oder es wird sofortterminate()aufgerufen.Wenn ich das jetzt also richtig verstanden habe, dann ist das Verhalten beim Bruch der neuen
noexcept(true)-Spezifikation implizit das gleiche, dass auch beim Bruch derthrow()-Spezifikation ohne alternativenunexpected-Handler statfindet.Nicht ganz. Die "Stackunwinding"-Garantie fällt bei noexcept(true) weg.
Yamakuzure schrieb:
Jetzt klar? Und wenn ja, habe ich das nun endlich richtig verstanden?
Ich denke schon. Ich will aber auch selbst nicht ausschliessen, etwas falsch verstanden zu haben.
Yamakuzure schrieb:
Krümelkacker schrieb:
Wie Du das daraus entnehmen kannst, verstehe ich nicht. Ich glaube eher, Du hast das mit den "implicit calls" missverstanden.
Ich habe den Text so verstanden, dass
noexcept(foo())auch danntruezurückgibt, wenn foo() nichts weiter tut, als eine Funktion aufzurufen, die nicht werfend darf. Also in etwa so:void ichWerfNicht () noexcept { /* tue irgendwas absolut sicheres */ } void foo() { /* tue irgendwas absolut sicheres */ ichWerfNicht(); /* tue irgendwas absolut sicheres */ } /* irgendwo anders: */ bool wasBinIch = noexcept(foo()); // Muesste true ergeben ?!?Nein. f() ist ein "potentially evaluated call to a function [...] that does not have a non-throwing exception-specification". Daher liefert noexcept(f()) false. Alles andere wäre auch wahrscheinlich nicht wirklich praktikabel. Denk mal nur an Funktionsdeklarationen, die im Header auftauchen und in einer cpp-Datei definiert werden. Soll dann bei getrennter Übersetzung -- falls die Definition der Funktion in der aktuellen ÜE enthalten ist -- der noexcept Operator true liefern (wenn der Compiler feststellen kann, dass nichts fliegen kann) und in einer anderen ÜE false, weil nur die Deklaration bekannt ist? Wenn man diese Inkonsistenzen vermeiden will, kann man sich entweder auf die Funktions-Deklarationen und den Ausnahmespezifizierungen beschränken (so wie es im aktuellen Entwurf geschieht) oder man muss dem Compiler eine Art globale Analyse aufzwingen, falls er öfters mal true liefern will. Oder plädierst Du für inkonsistente noexcept-Ergebnisse, die davon abhängen, ob die Definition in der aktuellen ÜE bekannt ist? Das würde ich für eine schlechte Idee halten.
kk
-
@Krümelkacker: Ah! Danke nochmals!
@Shade: Das noexcept den Code schneller macht, halte ich ersteinmal für ein Gerücht, aber das es den Code nicht ausbremst wie die klassische Ausnahmespezifikation ist mir auch klar.
Was ich nicht verstehe ist, warum Ihr alle so scharf darauf seid auf Stack-Unwinding zu verzichten? Aber das führt langsam mal zu weit.
Mit _einer_ Sache bin ich aber eben nicht einverstanden:
Shade Of Mine schrieb:
Weiters hast du auch den Kontext des Fehlers nicht, du kannst also auf nichts reagieren. (...)Du weisst nur: irgendwo sind Invarianten verletzt, ich kann meinen Daten nicht mehr trauen.
Im Programm, hast du natürlich absolut Recht. Aber wenn man einen verbose terminate handler verwendet, hat man a) Stack-Unwinding, und b) den Namen der Exception. Und wenn du gerade in einer VM mit PuTTY auf einer AIX Maschine am anderen Ende der Welt unterwegs bist, dir nur VI mit zwei Farben (Kein Klickibunti, keine Ctags, nix) und IBM vacc zur Verfügung steht, es zwei Stunden nach Feierabend ist, morgen der Cut-Over stattfindet, und eines der Programme, die unbedingt laufen müssen aussteigt, dann freust du dich wie ein Schneekönig über einen Abbruch wie:
terminate called after throwing an instance of 'docIdNotFoundInTOA01'Warum? weil du dann sofort weißt, wo du nachschauen musst. Ein generelles catch auf std::exception bringt dir dann nämlich nichts, und du müsstest Wer-weiß-was anstellen um überhaupt erst einmal eine Idee zu Erhalten, wo du suchen musst.
Bevor du jetzt aber immer noch glaubst, ich versuche hier irgendwem weißzumachen, dass
throw()ganz toll undnoexceptganz schlecht sei (*), nein. Denn die obige Ausgabe habe ich mitnoexceptauch. Gut, ich habe dafür auch gleich noch eine unbestimmte Anzahl Speicherlecks dazu, aber das ist dann halt Pech des Entwicklungszyklus. Erkauft mit ein wenig Performance, auf die sich zahlende Kunden und die TÜV-IT ein Ei backen.(*) ...und das habe ich nun schon mehrfach betont.
-
Yamakuzure schrieb:
Was ich nicht verstehe ist, warum Ihr alle so scharf darauf seid auf Stack-Unwinding zu verzichten? Aber das führt langsam mal zu weit.
terminate() macht ja auch kein Stack Unwind. Stack Unwind ist oft eine ziemlich blöde Sache die man vermeiden will.
Im Programm, hast du natürlich absolut Recht. Aber wenn man einen verbose terminate handler verwendet, hat man a) Stack-Unwinding, und b) den Namen der Exception. Und wenn du gerade in einer VM mit PuTTY auf einer AIX Maschine am anderen Ende der Welt unterwegs bist, dir nur VI mit zwei Farben (Kein Klickibunti, keine Ctags, nix) und IBM vacc zur Verfügung steht, es zwei Stunden nach Feierabend ist, morgen der Cut-Over stattfindet, und eines der Programme, die unbedingt laufen müssen aussteigt, dann freust du dich wie ein Schneekönig über einen Abbruch wie:
terminate called after throwing an instance of 'docIdNotFoundInTOA01'Das ist mit StackGuards aber auch absolut kein Thema. Nur dass du Kontext hast.
Stell dir als Exception einfach OutOfBounds() vor. Da ist der Kontext essentiell viel Wert. Natürlich kann man auch mit terminate etwas herumtricksen, aber Prinzipiell kannst du dann einfach auch einen StackGuard verwenden und fährst besser.Mal davon abgesehen solche Logikfehler eh quasi nie auftreten. Die Probleme liegen idR woanders. Denn nothrow Funktionen sind idR so trivial dass es da keine Fehler mit wirft/wirft nicht gibt. Wir reden also von einer Fehlerquote jenseits von gut und böse...
Nochmals:
throw() bietet dir keine Vorteile. Du zählst zwar Sachen auf die throw() dir bietet, aber ein StackGuard bietet dir mehr, ist weniger Aufwand (kein komplexes Unwind im terminate()) und steht nicht im Interface der Funktion. Und das wichtigste: es garantiert dir Fehlerfreien Code.Stack Unwind im terminate kann Fatal sein. Deshalb würde ich es nie machen.
-
SOM schrieb:
Mal davon abgesehen solche Logikfehler eh quasi nie auftreten. Die Probleme liegen idR woanders.
*husthust* das war ein Beispiel aus meinem Arbeitsalltag. Schöööön das du weißt, das sowas nie auftritt. Aber der Keks, der vor X Jahren dieses bestimmte Programm geschrieben hat, war so lieb und hat für diese "Ausgabe" gesorgt, so dass ich den Krempel fertig bekommen habe.
Außerdem wüßte ich gerne, welches "StackGuard" du meinst? Den Protektor gegen Stack Smashing von Immunix, oder die Erweiterung zu gcc, die gegen Buffer Overflows schützen soll? Gibts noch andere Programme die so heißen? Und existieren die dann auch nicht für AIX+VACC? Klar, es gibt ProPolice/ssp für AIX. Aber das ist eine modifizierte Version von gcc und kein VACC mehr, der aber gefordert war. Und wie hätte mir ein SSP helfen sollen? Und wie hätte ich dem Kunden erklären sollen, er solle doch bitte alle Sicherheitskonzepte über den Haufen werfen, und mir ein paar hübsche Extraproggies installieren?
Aber weißt du was? Du hast Recht, ein solches Szenario gibt es nie und wird es nie geben, und ich habe meine Ruhe.
P.S.: Da es geflissentlich überlesen wurde, hier nochmals: Ich verwende niemals Ausnahmespezifikationen, außer es handelt sich um Bibliotheks- / ToolKit- oder FrameWork-Entwicklung. Und selbst da nur ganz ganz am Anfang um das Design zu prüfen.
-
Yamakuzure schrieb:
*husthust* das war ein Beispiel aus meinem Arbeitsalltag. Schöööön das du weißt, das sowas nie auftritt.
Habe nie behauptet dass es nie auftritt. Aber sei mal ehrlich: wie oft hattest du das Problem dass eine nothrow Funktion etwas geworfen hat? Das tritt so selten auf, dass es sich nicht lohnt ewig darueber zu diskutieren

Außerdem wüßte ich gerne, welches "StackGuard" du meinst?
Ich rede von C++ Code. Eine StackGuard Klasse oder ScopeGuard Klasse macht im Prinzip nichts anderes als im Dtor eine bestimmte Aktion ausfuehren.
Eine moegliche Implementierung koennte so aussehen:
class ForbidThrows { private: char const* function; char const* file; size_t line; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line) {} ~ForbidThrows() { if(uncaught_exception()) { write_log("unallowed exception thrown", function, file, line); terminate(); } } }; #define FORBIDTHROWS ForbidThrows __guard(__FUNC__, __FILE__, __LINE__) void foo() { FORBIDTHROWS; throw "error"; }Alternativ wird auch oefters mal try/catch in einem Makro versteckt verwendet, ich mag das persoenlich weniger - aber es funktioniert auch.
Der Vorteil: du hast den Kontext den du beim unhandled exception handler nicht hast.
-
Shade Of Mine schrieb:
void foo() { FORBIDTHROWS; throw "error"; }...was, wenn foo mit noexcept deklariert worden wäre, nicht mehr garantiert zu der gewünschten Ausgabe führt. Nur mal so am Rande erwähnt, falls das jemandem noch nicht klar war.
-
krümelkacker schrieb:
...was, wenn foo mit noexcept deklariert worden wäre, nicht mehr garantiert zu der gewünschten Ausgabe führt. Nur mal so am Rande erwähnt, falls das jemandem noch nicht klar war.
Jep. Es ist ein Entweder/Oder und kein Und.
-
Shade Of Mine schrieb:
Eine moegliche Implementierung koennte so aussehen:
class ForbidThrows { private: char const* function; char const* file; size_t line; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line) {} ~ForbidThrows() { if(uncaught_exception()) { write_log("unallowed exception thrown", function, file, line); terminate(); } } }; #define FORBIDTHROWS ForbidThrows __guard(__FUNC__, __FILE__, __LINE__) void foo() { FORBIDTHROWS; throw "error"; }Das ist gefährlicher Unsinn.
Beispiel: wir wollen Bar() so implementieren dass keine Exceptions geworfen werden können. Zur "Sicherheit" machen wir dein Makro rein:#include <iostream> #include <exception> class ForbidThrows { private: char const* function; char const* file; size_t line; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line) {} ~ForbidThrows() { if(std::uncaught_exception()) { std::cout << "BOOM!" << std::endl; std::terminate(); } } }; #define FORBIDTHROWS ForbidThrows __guard(__FUNCTION__, __FILE__, __LINE__) // ------------------------------ void Bar() { FORBIDTHROWS; // code der keine Exceptions werfen kann } // bloss irgend eine klasse class SomeThing { public: ~SomeThing() { // nach dem Bar() "safe" ist, können wir es hier im dtor aufrufen ohne Probleme zu bekommen Bar(); } }; int main() { try { SomeThing thing; // hier jetzt Code der eine Exception werfen kann throw std::runtime_error("something happened"); } // BOOM! catch (std::exception const& e) { std::cout << e.what() << std::endl; } }Erklärung: Bar() wird von SomeThing::~SomeThing() aus aufgerufen, und SomeThing::~SomeThing wiederum läuft während eine noch-nicht-gefangene Exception rumfliegt. std::uncaught_exception() liefert also true, obwohl Bar() selbst keine Exception geworfen hat.
Dein Makro müsste also besser MAY_NOT_BE_CALLED_WHILE_UNWINDING heissen, denn sobald es in Code auftaucht der während Stack-Unwinding ausgeführt werden kann, knallt es.
Wer solche Konstrukte in seinen Programmen verwendet, sollte sich also darauf gefasst machen, dass seine Programme einfach so sterben gehen, obwohl nichts wirklich schlimmes passiert ist.
----
std::uncaught_exception() ist eine der C++ Funktionen die man am besten gleich wieder vergessen sollte, denn man kann damit nichts vernünftiges machen.
-
Hatte vergessen im Ctor den Status von uncaught_exception() zu überprüfen.
class ForbidThrows { private: char const* function; char const* file; size_t line; bool unwind; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line), unwind(std::uncaught_exception()) {} ~ForbidThrows() { if(unwind != std::uncaught_exception()) { std::cout << "BOOM!" << std::endl; std::terminate(); } } };Ich habe leider die originalen Codes nicht bei mir, aber etwas in der Art verwende ich zum loggen solcher Fehler.
uncaught_exception ist etwas ziemlich praktisches - es ist verpönt weil viele Leute es als indikator verwenden ob sie werfen dürfen oder nicht.
Aber wie gesagt, wenn du das nicht willst, gibt es auch die Variante mit 2 markos:
#define NOTHROW_BEGIN try{ #define NOTHROW_END catch(...){log();terminate();} void foo() { NOTHROW_BEGIN; bla(); NOTHROW_END; }ist auch besser als throw() zu verwenden.
-
uncaught_exception ist etwas ziemlich praktisches - es ist verpönt weil viele Leute es als indikator verwenden ob sie werfen dürfen oder nicht.
Nein, uncaught_exception ist verpönt weil es konzeptionell kaputt ist!
Die Variante mit im Konstruktor prüfen funktioniert auch nicht richtig. Die ignoriert nämlich Fehler während des Stack-Unwinding komplett. Das kann's ja wohl auch nicht sein.
Damit man es für irgendwas vernünftig verwenden könnte müsste uncaught_exception die Exception-Nesting-Depth zurück geben. Tut es halt leider nicht.
-
hustbaer schrieb:
Nein, uncaught_exception ist verpönt weil es konzeptionell kaputt ist!
Was ist denn Kaputt?
Die Variante mit im Konstruktor prüfen funktioniert auch nicht richtig. Die ignoriert nämlich Fehler während des Stack-Unwinding komplett. Das kann's ja wohl auch nicht sein.
Wenn beim unwind eine Exception fliegt, dann wird sowieso terminate aufgerufen. Oder wie willst du darauf reagieren?
Damit man es für irgendwas vernünftig verwenden könnte müsste uncaught_exception die Exception-Nesting-Depth zurück geben. Tut es halt leider nicht.
es gibt nur eine depth von 1. mehr geht nicht. bei einer depth von 2 gibts kein unwind mehr.
-
Mal davon abgesehen, dass mir dieses "StackGuard"-Dingen nicht sonderlich gefällt...
hustbaer schrieb:
Shade Of Mine schrieb:
...
Das ist gefährlicher Unsinn.
Beispiel: wir wollen Bar() so implementieren dass keine Exceptions geworfen werden können. Zur "Sicherheit" machen wir dein Makro rein:#include <iostream> #include <exception> class ForbidThrows { private: char const* function; char const* file; size_t line; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line) {} ~ForbidThrows() { if(std::uncaught_exception()) { std::cout << "BOOM!" << std::endl; std::terminate(); } } }; #define FORBIDTHROWS ForbidThrows __guard(__FUNCTION__, __FILE__, __LINE__) // ------------------------------ void Bar() { FORBIDTHROWS; // code der keine Exceptions werfen kann } // bloss irgend eine klasse class SomeThing { public: ~SomeThing() { // nach dem Bar() "safe" ist, können wir es hier im dtor aufrufen ohne Probleme zu bekommen Bar(); } }; int main() { try { SomeThing thing; // hier jetzt Code der eine Exception werfen kann throw std::runtime_error("something happened"); } // BOOM! catch (std::exception const& e) { std::cout << e.what() << std::endl; } }[...]
std::uncaught_exception() ist eine der C++ Funktionen die man am besten gleich wieder vergessen sollte, denn man kann damit nichts vernünftiges machen.
Wie sähe es denn damit aus:
class ForbidThrows { private: char const* function; char const* file; size_t line; bool already_unwinding_before; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line), already_unwinding_before( std::uncaught_exception() ) {} ~ForbidThrows() { if(!already_unwinding_before && std::uncaught_exception()) { std::cout << "BOOM!" << std::endl; std::terminate(); } } };?
Ich sehe trotzdem noch nicht ganz den Sinn in der Sache. Wann soll/kann das jetzt eingesetzt werden, Shade?
kk
-
krümelkacker schrieb:
Ich sehe trotzdem noch nicht ganz den Sinn in der Sache. Wann soll/kann das jetzt eingesetzt werden, Shade?
Statt throw() als nothrow spezifikation.
Sprich: wenn die Funktion etwas wirft obwohl sie nichts werfen sollte. Sinn und Unsinn warum ich das wissen will, sei mal dahingestellt. Aber darum ging es hier: die throw() Spezifikation mit etwas sinnvollerem zu ersetzen.Denn bei throw() hat man keinen Kontext, bei diesem StackGuard schon.
Ich verwende das wie gesagt dazu um festzustellen ob eine Funktion wegen einer Exception beendet wurde oder nicht. Das liefert mir beim Kunden ganz ordentliche traces.
Prinzipiell sage ich damit nur aus: throw() sollte nicht verwendet werden. Ob man diesen StackGuard oder das try/catch Äquivalent oder nichts verwenden will ist jedem freigestellt. Nur denke ich dass throw() die schlechteste aller alternativen (da es keine Vorteile aber Nachteile bietet).
-
Also die Idee finde ich insofern gut, als dass sie zumindest als Inspiration dienlich ist. Einbauen würde ich sowas nicht. (Meine Ausnahmen geben immer an wo sie geworfen wurden, in welchem Kontext, wann, und durch welchen "Weg" sie weitergeleitet wurden. Also "Called by fileA.cpp:123 - funcA(), Called by ...".
Ich gehöre übrigens zu den Leuten, die
throw(except),try { singleFunc; }, undcatch(except &e) { addTrace(); throw; }in Makros packen. Der Einzige Vorteil ist die Lesbarkeit, mehr nicht.Aber zurück zu den großen Mißverständnissen:
Deine Frage war, wie oft mir eine Ausnahmespezifikation in meinem leben schon geholfen hat. Nunja, erst zwei Mal, dafür richtig. Betonen möchte ich, dass besagtes AIX-Programm das einzige ist, mit dem ich jemals zu tun hatte, das überhaupt Ausnahmespezifikationen in der Releaseversion hat. Und es war auch nur drei mal throw(). Und alle drei standen drin, weil die entsprechenden Methoden andere aufrufen, die unter bestimmten Umständen Ausnahmen werfen, diese Umstände aber ausschließen. Im Falle der "kaputten" Methode wurden DocIDs aus einer Quelltabelle gelesen, die garantiert auch in der TOA01 stehen. Statt sich also mit einem (auf Jahre überflüssigen) try-catch zu belasten, hatte der original-Autor die Methode mit
throw()spezifiziert, und einen Kommentar hinzugefügt, dass die Quelltabelle wohl geändert wurde, sollte es hier jemals zu einemterminate()kommen. (Hatte er auch Recht mit. ;))Bei Bibliotheken, Toolkits und Frameworks benutze ich Ausnahmespezifikationen, und zwar die
throw(a, b, c...)-Variante, ausschließlich am Anfang, um das Design zu überprüfen. In diesem Bereich ist es dein Hintern der dran ist, wenn ein Kunde deine Doku Wortgetreu beachtet, und sein Programm stirbt, weil deine Bibliothek etwas schmeißt, was sie laut deiner Dokumentation nicht darf.
Außerdem finde ich es praktisch nur in einem Header die Liste der Funktionsdeklarationen zu Rate ziehen zu müssen, um zu wissen, welche schlauen Ausnahmen ich mir im Design überlegt habe. Aber "geknallt" hat es bislang erst einmal.Bitte nicht falsch verstehen: Im Produktivbetrieb sind Ausnahmespezifikation meiner Meinung nach ein Zeichen für "nicht fertig". Und nachdem ich hier gelernt habe, dass sie auch noch gewaltigen Overhead produzieren, um so mehr.
Was mir aber immer noch Sorgen bereitet ist die Vorstellung, dass gerade die
throw(a, b, c u.s.w.)-Variante ja ganz wegfallen wird. Irgendjemand schrieb mal, dass es für die Überprüfung Programme gäbe, aber welches Programm kann denn Gedanken lesen und mir sagen, dass eine Funktion eine Ausnahme "durchgelassen" (darum gehts!)(*) hat, die sie hätte abfangen müssen? Da wäre ich über einen Tipp in der Tat sehr dankbar.(*) Edith möchte erklären, das es sinnvoller ist, einen zu einer Ausnahme führenden Sonderfall vor dem Aufruf der vielleicht werfenden Funktion abzufangen, als alles in try/catch zu packen. Und: tadaaa! Auch aus Performancegründen.
-
Shade Of Mine schrieb:
hustbaer schrieb:
Die Variante mit im Konstruktor prüfen funktioniert auch nicht richtig. Die ignoriert nämlich Fehler während des Stack-Unwinding komplett. Das kann's ja wohl auch nicht sein.
Wenn beim unwind eine Exception fliegt, dann wird sowieso terminate aufgerufen. Oder wie willst du darauf reagieren?
Na man könnte sie fangen.
Damit man es für irgendwas vernünftig verwenden könnte müsste uncaught_exception die Exception-Nesting-Depth zurück geben. Tut es halt leider nicht.
es gibt nur eine depth von 1. mehr geht nicht. bei einer depth von 2 gibts kein unwind mehr.
Falsch, natürlich können Exceptions "stacken". Anders gesagt: es kann beliebig viele "aktuelle" Exceptions (pro Thread) geben, die sozusagen "in der Luft hängen" (geworfen wurden aber noch nicht gefangen).
Wäre auch katastrophal wenn es nicht so wäre. Das würde bedeuten dass man z.B. in einem Destruktor keine Logging-Funktionen mehr verwenden könnte, sobald die Logging-Funktionen z.B. IO Klassen verwenden, die Exceptions werfen könnten. Auch wenn die Logging-Funktionen die Exceptions brav fangen und nicht weiterfliegen lassen.
Aber probier es aus, hier ein 1:1 compilierbares Beispiel:
#include <iostream> void unwind_test_0(); void unwind_test_1(); void unwind_test_2(); void do_something(); struct my_exception { my_exception(int a, int b, int c) : m_a(a), m_b(b), m_c(c) {} void dump() const { std::cout << "a = " << m_a << ", b = " << m_b << ", c = " << m_c << std::endl; } int m_a; int m_b; int m_c; }; void unwind_test_0() { struct helper { ~helper() { unwind_test_1(); } }; try { helper h; std::cout << "unwind_test_0 throw" << std::endl; throw my_exception(1, 2, 3); } catch (my_exception const& e) { std::cout << "unwind_test_0 catch" << std::endl; e.dump(); } } void unwind_test_1() { struct helper { ~helper() { unwind_test_2(); } }; try { helper h; std::cout << "unwind_test_1 throw" << std::endl; throw my_exception(10, 20, 30); } catch (my_exception const& e) { std::cout << "unwind_test_1 catch" << std::endl; e.dump(); } } void unwind_test_2() { struct helper { ~helper() { do_something(); } }; try { helper h; std::cout << "unwind_test_2 throw" << std::endl; throw my_exception(100, 200, 300); } catch (my_exception const& e) { std::cout << "unwind_test_2 catch" << std::endl; e.dump(); } } void do_something() { std::cout << "do_something" << std::endl; } int main() { unwind_test_0(); std::cout << "nix passiert, alles wieder gut" << std::endl; return 0; }Hier fliegen drei Exceptions gleichzeitig.
Output:unwind_test_0 throw unwind_test_1 throw unwind_test_2 throw do_something unwind_test_2 catch a = 100, b = 200, c = 300 unwind_test_1 catch a = 10, b = 20, c = 30 unwind_test_0 catch a = 1, b = 2, c = 3 nix passiert, alles wieder gutGenerall kann man sagen: du kanns jeden beliebigen Code während des Stack-Unwinding laufen lassen, er wird sich nicht anders verhalten, als wenn er in main() stehen würde. D.h. wenn eine Exception aus diesem Code "austreten" würde, dann wird
terminate()aufgerufen. Und wenn nicht, weil alle Exceptions wieder irgendwo gefangen werden, dann geht eben alles so wie es sonst auch gehen würde.Probier ein x-beliebiges Programm von dir, welches Exceptions wirft und wieder fängt, in
do_something()laufen zu lassen. Bis auf diversestd::uncaught_exceptionHacks sollte *alles* so laufen wie sonst auch. Und das obwohl gerade drei Exceptions "in der Luft hängen".@krümelkacker et al:
Nö, geht nicht. Grund siehe oben.
Das "verbesserte" ForbidThrows wäre nicht in der Lage inunwind_test_1, unwind_test_2oderdo_somethingzu erkennen, dass die Funktion, die es "schützen" sollte, doch eine Exception geworfen hat. -> sinnlosUnd bevor mir jetzt jemand schreibt, dass das ein konstruiertes Beispiel ist: Funktionen mit "no-throw" Garantie macht man nicht zuletzt dazu, damit man sie während des Unwinding gefahrlos aufrufen kann. Beispiel: "undo" von Änderungen um all-or-nothing Transaktionen zu implementieren.
Die Nutzung solcher Funktionen in Codeteilen, die während des Stack-Unwinding laufen, ist also keineswegs ungewöhnlich. Und wenn ein Debugging-Feature in diesem nicht ungewöhnlichen Fall dann einfach so versagt, dann ist es für mich wertlos. Bzw. sogar gefährlich, wenn man sich darüber nicht im Klaren ist, und sich falscherweise darauf verlässt.
Würde
std::uncaught_exceptiondagegen die Nesting-Tiefe zurückliefern, dann könnte man einfach aufstd::uncaught_exception() > m_exception_nesting_depth_in_ctorprüfen.
-
#include <iostream> class ForbidThrows { private: char const* function; char const* file; size_t line; bool unwind; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line), unwind(std::uncaught_exception()) {} ~ForbidThrows() { if(unwind != std::uncaught_exception()) { std::cout << "BOOM!" << std::endl; std::terminate(); } } }; #define FORBIDTHROWS ForbidThrows __guard(__FUNCTION__, __FILE__, __LINE__) bool doThrow; void unwind_test_0(); void unwind_test_1(); void unwind_test_2(); void do_something(); struct my_exception { my_exception(int a, int b, int c) : m_a(a), m_b(b), m_c(c) {} void dump() const { std::cout << "a = " << m_a << ", b = " << m_b << ", c = " << m_c << std::endl; } int m_a; int m_b; int m_c; }; void unwind_test_0() { FORBIDTHROWS; struct helper { ~helper() { unwind_test_1(); } }; try { helper h; std::cout << "unwind_test_0 throw" << std::endl; throw my_exception(1, 2, 3); } catch (my_exception const& e) { std::cout << "unwind_test_0 catch" << std::endl; e.dump(); } } void unwind_test_1() { FORBIDTHROWS; struct helper { ~helper() { unwind_test_2(); } }; try { helper h; std::cout << "unwind_test_1 throw" << std::endl; throw my_exception(10, 20, 30); } catch (my_exception const& e) { std::cout << "unwind_test_1 catch" << std::endl; e.dump(); } } void unwind_test_2() { FORBIDTHROWS; struct helper { ~helper() { do_something(); } }; try { helper h; std::cout << "unwind_test_2 throw" << std::endl; throw my_exception(100, 200, 300); } catch (my_exception const& e) { std::cout << "unwind_test_2 catch" << std::endl; e.dump(); } if(doThrow) throw my_exception(1000,2000,3000); } void do_something() { FORBIDTHROWS; std::cout << "do_something" << std::endl; } int main() { doThrow=false; unwind_test_0(); std::cout << "nix passiert, alles wieder gut" << std::endl; doThrow=true; unwind_test_0(); std::cout << "alles kaputt :(" << std::endl; return 0; }funktioniert.
bzw. sobald die 2. exception ausserhalb vom try fliegt, gibts n terminateunwind_test_0 throw unwind_test_1 throw unwind_test_2 throw do_something unwind_test_2 catch a = 100, b = 200, c = 300 unwind_test_1 catch a = 10, b = 20, c = 30 unwind_test_0 catch a = 1, b = 2, c = 3 nix passiert, alles wieder gut unwind_test_0 throw unwind_test_1 throw unwind_test_2 throw do_something unwind_test_2 catch a = 100, b = 200, c = 300 terminate called after throwing an instance of 'my_exception' Abort trap
-
Shade, denk mal bitte ein wenig nach.
In deinem Beispiel wird terminate aufgerufen, weil du eine Exception wirfst die während des Unwinding einen Destruktor verlässt.
Der Aufruf von terminate() hat nichts mit deinemForbidThrowsGurad zu tun, den kannst du genau so gut weglassen.
Probier es aus, setz einen Breakpoint in den Ctor und Dtor vonForbidThrows.
-
hustbaer schrieb:
In deinem Beispiel wird terminate aufgerufen, weil du eine Exception wirfst die während des Unwinding einen Destruktor verlässt.
Und das ist genau das was passieren soll.
ForbidThrow ruft nicht terminate auf. Muss es auch nicht. Sein einziges Ziel in dieser Welt ist zu verhindern dass eine nothrow Funktion etwas wirft. Und dann als Nebenpunkt den nötigen Kontext bringen um festzustellen warum das passiert ist.Der Guard kann gegen 2 geworfene Exceptions nicht schützen, nichts kann dagegen Schützen. Er will ja auch garnicht dagegen schützen. Auch ein unexpected Handler schützt hier nicht. In dem Fall wird er zwar aufgerufen, was in der Tat ein Vorteil gegenüber der StackGuard Klasse ist (aber keiner gegenüber dem try/catch) aber man kann weder recovern noch sonst was sinnvolles tun.
Das ist keine Klasse die alle Fehler korrigiert. Sie passt nur auf, dass du keine Exception aus einer nothrow Funktion herauslässt. Nicht mehr und nicht weniger. Und das tut sie.
-
Shade Of Mine schrieb:
Sie passt nur auf, dass du keine Exception aus einer nothrow Funktion herauslässt. Nicht mehr und nicht weniger. Und das tut sie.
Naja, eben nicht.
#include <iostream> // ------------------------------------------- class ForbidThrows { private: char const* function; char const* file; size_t line; bool unwind; public: ForbidThrows(char const* function, char const* file, size_t line) : function(function), file(file), line(line), unwind(std::uncaught_exception()) {} ~ForbidThrows() { if(unwind != std::uncaught_exception()) { std::cout << "BOOM!" << std::endl; std::terminate(); } } }; #define FORBIDTHROWS ForbidThrows __guard(__FUNCTION__, __FILE__, __LINE__) // ------------------------------------------- struct my_exception { my_exception(int a, int b, int c) : m_a(a), m_b(b), m_c(c) {} void dump() const { std::cout << "a = " << m_a << ", b = " << m_b << ", c = " << m_c << std::endl; } int m_a; int m_b; int m_c; }; // ------------------------------------------- bool doThrow; void nothrow_function() { FORBIDTHROWS; if (doThrow) { std::cout << "nothrow_function: we're going to throw anyway..." << std::endl; throw my_exception(1, 2, 3); } } // ------------------------------------------- int veryImportantState = 0; void client_function() { try { std::cout << "client_function: before calling nothrow_function..." << std::endl; nothrow_function(); veryImportantState = 0; // da nothrow_function() nix werfen "kann", muss das hier ja noch ausgeführt werden std::cout << "client_function: after calling nothrow_function." << std::endl; } catch (...) { // hier wollten wir eigentlich nur exceptions fangen die irgendwann NACH der // ausführung von nothrow_function() geworfen wurden, da wir uns ja darauf verlassen // dass nothrow_function() nix wirft. } } struct some_class { some_class() { veryImportantState = 1; } ~some_class() { std::cout << "some_class::~some_class" << std::endl; client_function(); } }; void test_function() { try { some_class sc; throw my_exception(1, 2, 3); } catch (my_exception const& e) { e.dump(); } } int main() { std::cout << "with well behaved nothrow_function()..." << std::endl << std::endl; test_function(); std::cout << "veryImportantState = " << veryImportantState << std::endl; // --- std::cout << std::endl << "with misbehaved nothrow_function()..." << std::endl << std::endl; doThrow=true; test_function(); std::cout << "veryImportantState = " << veryImportantState << " - OOPS!" << std::endl; return 0; }Output:
with well behaved nothrow_function()... some_class::~some_class client_function: before calling nothrow_function... client_function: after calling nothrow_function. a = 1, b = 2, c = 3 veryImportantState = 0 with misbehaved nothrow_function()... some_class::~some_class client_function: before calling nothrow_function... nothrow_function: we're going to throw anyway... a = 1, b = 2, c = 3 veryImportantState = 1 - OOPS!