Schnitstellen in C++?



  • 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; } , und catch(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 einem terminate() 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 gut
    

    Generall 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 diverse std::uncaught_exception Hacks 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 in unwind_test_1, unwind_test_2 oder do_something zu erkennen, dass die Funktion, die es "schützen" sollte, doch eine Exception geworfen hat. -> sinnlos

    Und 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_exception dagegen die Nesting-Tiefe zurückliefern, dann könnte man einfach auf std::uncaught_exception() > m_exception_nesting_depth_in_ctor prü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 terminate

    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 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 deinem ForbidThrows Gurad zu tun, den kannst du genau so gut weglassen.
    Probier es aus, setz einen Breakpoint in den Ctor und Dtor von ForbidThrows .



  • 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!
    

Anmelden zum Antworten