Fragen zu Exceptions - Lebensdauer



  • Hallo zusammen,

    ich beschäftige mich gerade mit Exceptions und habe dazu die drei Artikel im Magazin gelesen. Zur Übung wollte ich dann mal eigene Exception-Klassen schreiben.
    Allerdings wollte ich dabei schon wieder viel zu viel auf einmal und ich musste feststellen, dass mir doch noch einige Basics zu dem Thema fehlen. 🙄

    Ich spule also nochmal zurück und beginne heute mit der Lebensdauer einer Exception.

    Im ersten Artikel des Magazins habe ich gesehen, dass Exceptions oft als Referenz gefangen werden und ich habe etwas rumgetestet (ich habe extra kein const im catch, wegen der letzten Frage):

    void throwInt(int a){
        switch(a) {
        case 1: throw a;
        case 2: throw 2;
        default: throw int();
        }
    }
    
    int main(){
        try{
            throwInt(1);
        } catch(int& ex){
            cout << ex << " gefangen\n";
        }
        return EXIT_SUCCESS;
    }
    

    Das funktioniert alles. Aber warum? Ich habe dazu folgende Fragen:

    • Gibt es Unterschiede zwischen den drei throw-Anweisungen (abgesehen vom Inhalt von ex)?
    • Wie ist die Lebensdauer eines geworfenen Objektes? Zumindest in den Fällen 1 und default handelt es sich doch um lokale Objekte innerhalb von throwInt(), die eigentlich bei throw zerstört werden sollten.
    • Warum kann ich überhaupt eine Konstante werfen und mit einer (nicht konstanten) Referenz wieder fangen? Folgendes geht ja auch nicht:
    int getInt(){
        return 42;
    }
    
    int main(){
        int &x = getInt();      //geht nicht
        int const& y = getInt();//geht
        return 0;
    }
    

    Je mehr ich darüber nachdenke, um so mehr wundert es mich, dass das mit Referenzen überhaupt geht 😕

    Im Internet habe ich leider nur die Vorteile vom catch-by-reference gefunden, aber nicht warum das überhaupt möglich ist.
    Über eure Hilfe würde ich mich sehr freuen.

    Viele Grüße
    Matze



  • Je mehr ich darüber nachdenke, um so mehr wundert es mich, dass das mit Referenzen überhaupt geht

    Weil es so designed wurde. Exceptionobjekte leben nicht auf dem Programmstack. new erzeugt auch keine Objekte auf dem Stack, throw ebenfalls nicht.



  • Hallo knivil,

    als Noob schließe ich daraus, dass demnach bei

    MyClass a = 42;
    throw a;
    

    der Kopierkonstruktor aufgerufen wird. Immerhin habe ich a doch zuerst auf dem Stack angelegt.
    Was bringt mir der catch-by-reference dann (wenn ich nicht gerade die Basisklasse von MyClass abfrage)?

    Wo leben dann die Exceptionobjekte? Ebenfalls auf dem Heap?

    Gruß
    Matze



  • Was bringt mir der catch-by-reference

    Es wird kein Kopierkonstruktor beim Fangen aufgerufen, der vielleicht auch eine Exception werfen koennte. Ganz schlecht.

    Ebenfalls auf dem Heap?

    Rufst du new oder throw auf? Mit new werden Objekte auf dem Heap erzeugt. Exceptions leben "woanders" und werden von der Laufzeitumgebung verwaltet. Wie, wo, etc. sagt vielleicht der Standard, ich kann dazu nicht mehr sagen.


  • Mod

    Wo leben dann die Exceptionobjekte? Ebenfalls auf dem Heap?

    Die Exception-Objekte sind einfach Temporaries. Wo sie liegen ist nicht spezifiziert.

    §15.1/4 schrieb:

    The memory for the exception object is allocated in an unspecified way, except as noted in 3.7.4.1.

    Edit: In §3.7.4.1 steht lediglich

    In particular, a global allocation function is not called to allocate storage [...] for an exception object (15.1).



  • knivil schrieb:

    new erzeugt auch keine Objekte auf dem Stack, throw ebenfalls nicht.

    knivil schrieb:

    Ebenfalls auf dem Heap?

    Rufst du new oder throw auf? Mit new werden Objekte auf dem Heap erzeugt. Exceptions leben "woanders" und werden von der Laufzeitumgebung verwaltet. Wie, wo, etc. sagt vielleicht der Standard, ich kann dazu nicht mehr sagen.

    So blöd fand ich die Frage jetzt nicht...

    Ich wusste nicht, dass durch throw ein neues Objekt erzeugt wird. Sonst hätte ich noch weiter rumgetestet. Habe jetzt folgenden Code ausprobiert, der mir mehr Klarheit verschafft:

    class MyClass {
        int copy;
    public:
        MyClass():copy(0){}
        MyClass(MyClass const & o) : copy(1){ cout<<"copy\n"; }
        ~MyClass() { if(copy) cout << "destruct\n"; }
    };
    
    void throwClass(){
        MyClass a;
    //    throw a; //copy
        throw MyClass(); //keine copy
    }
    
    int wrapp(){
        try{
            throwClass();    
        }
        catch(MyClass& ex){
            cout << "catched in wrapp\n";
            throw;
            //throw ex; //wuerde auch eine neues Objekt erzeugen
        }
    }
    
    int main(){
        try{
            wrapp();
        }
        catch(MyClass& ex){
            cout << "catched in main\n";
        }
        cout << "Ende\n";
        return 0;
    }
    

    Das beantwortet mir auch die Fragen bezüglich Lebensdauer (bis Ende des catch-Blocks nach dem letzten rethrow) und den unterschiedlichen Aufrufen in meinem ersten Code-Beispiel (case 1 und case 2 legen Kopien an, default nicht (oder?)). Also besser immer throw MyClass(); ...

    Allerdings würde das ja heißen, dass durch throw auch eine neue Exception ausgelöst werden könnte 😮
    Langsam verstehe ich, warum es in C++ kein Exception-Chaning gibt...

    Vielen Dank für eure Antworten!

    Gruß
    Matze



  • kein Exception-Chaning gibt...

    http://en.cppreference.com/w/cpp/error/throw_with_nested und Verwandte



  • MatzeHHC schrieb:

    Im Internet habe ich leider nur die Vorteile vom catch-by-reference gefunden, aber nicht warum das überhaupt möglich ist.
    Über eure Hilfe würde ich mich sehr freuen.

    Man fängt Exception per Reference, da du sonst ein Objekt slicest. Fehlerbehanldung ist als Hierarchy aufgebaut (myError erbt von logic_error erbt von exception etc...) würdest du die std::exception per Value fangen, würden dadurch die zusätzlichen Informationen von eventuellen Unterklassen abgeschnitten.



  • MatzeHHC schrieb:

    Allerdings würde das ja heißen, dass durch throw auch eine neue Exception ausgelöst werden könnte 😮
    Langsam verstehe ich, warum es in C++ kein Exception-Chaning gibt...

    Das throw löst keine neue Exception aus. Du denkst dabei wahrscheinlich an Exceptions im Konstruktor des Exception-Objektes. Bedenke aber, dass der Konstruktor aufgerufen wird, bevor die Exception ausgelöst wird. Wirft der Konstruktor eine Exception, kommt es nicht mehr zum werfen der Exception, da dieser Vorgang durch die neue Exception vorzeitig abgebrochen wird.



  • Und man könnte es so wie in Java machen:

    class MyException : public std::exception
    {
    	std::exception & chain; // wobei sich ein (Smart-)Pointer eher anbieten würde
    
    public:
    	MyException(std::string const& what, std::exception & chain)
    	: std::exception(what), chain(chain)
    	{}
    
    	// ...
    };
    
    int foo()
    {
    	try {
    		throw std::runtime_exception("Fail !");
    	} catch (std::exception & e) {
    		throw MyException("Mist, schon wieder", e);
    	}
    }
    

    Die Frage ist, ob das wirklich sinnvoll ist...



  • Skym0sh0 schrieb:

    Und man könnte es so wie in Java machen:

    class MyException : public std::exception
    {
    	std::exception & chain; // wobei sich ein (Smart-)Pointer eher anbieten würde
    	
    public:
    	MyException(std::string const& what, std::exception & chain)
    	: std::exception(what), chain(chain)
    	{}
    	
    	// ...
    };
    
    int foo()
    {
    	try {
    		throw std::runtime_exception("Fail !");
    	} catch (std::exception & e) {
    		throw MyException("Mist, schon wieder", e);
    	}
    }
    

    Die Frage ist, ob das wirklich sinnvoll ist...

    Würde hierbei nicht die alte exception ungültig werden, sobalt die MyException den catch-block verlässt? Denn für sowas gibt es ja schließlich std::exception_ptr



  • Wieso man gerne Exceptions per Referenz fängt?

    try
    {
        BlubI var;
        throw var;
    }
    catch(Blubi a) { }
    

    Beobachte mal, wie oft der CopyCtor von BlubI aufgerufen wird. Nämlich genau zweimal. Einmal wenn a mit var initialisiert werden soll und ebenfalls beim throw! D.h., throw kopiert das Exception-Objekt immer, egal wie du es fängst. Da var sein Gütligkeitsbereich verlässt, bleibt throw auch nichts anderes übrigt, es so zu tun.

    Um jetzt eine Kopie zu vermeiden, fängst du das Exception-Objekt das dir von throw übergeben wurde per Referenz.



  • @Flashput
    Ja, natürlich würde sie das.

    @Skym0sh0
    Wieso verzapfst du so oft solchen Unsinn hier? Was meinst du wozu exception_ptr erfunden wurde.

    @KasF
    Und zusätzlich will man oft den konkreten Typ fangen, und nicht eine geslicete Kopie der Basisklasse.



  • hustbaer schrieb:

    @Skym0sh0
    Wieso verzapfst du so oft solchen Unsinn hier? Was meinst du wozu exception_ptr erfunden wurde.

    Erstens was heisst oft?
    Zweitens ist mir vorhin exception_ptr nicht eingefallen
    Drittens ist es syntaktisch möglich, wenn ich mich nicht irre. Wie gesagt, der Sinn wurde von mir nicht hinterfragt. Mir ging es bei dem Beispiel nur ums theoretisch mögliche.



  • tntnet schrieb:

    Das throw löst keine neue Exception aus. Du denkst dabei wahrscheinlich an Exceptions im Konstruktor des Exception-Objektes. Bedenke aber, dass der Konstruktor aufgerufen wird, bevor die Exception ausgelöst wird. Wirft der Konstruktor eine Exception, kommt es nicht mehr zum werfen der Exception, da dieser Vorgang durch die neue Exception vorzeitig abgebrochen wird.

    Nein, ich denke an den Kopierkonstruktor, wenn man ein bereits bestehendes Objekt wirft:

    MyClass obj;
    throw obj;
    

    Sollte man wahrscheinlich vermeiden, oder?

    Warum man Exceptions als Referenz fängt war mir von Anfang an klar. Warum das überhaupt geht hingegen nicht 😉

    Mein ursprünglicher Plan war, C++98 zu lernen und mir dann nach und nach die C++11-Erweiterungen anzuschauen, aber das ist wohl eine blöde Idee, oder? Es hat für mich den Anschein, als wenn man mit C++11 ganz anders programmiert...

    Gruß
    Matze



  • MatzeHHC schrieb:

    Mein ursprünglicher Plan war, C++98 zu lernen und mir dann nach und nach die C++11-Erweiterungen anzuschauen, aber das ist wohl eine blöde Idee, oder? Es hat für mich den Anschein, als wenn man mit C++11 ganz anders programmiert...

    +1
    “C++11 feels like a new language.” (Bjarne Stroustrup)


Log in to reply