zweimaliger aufruf des destruktors aber nur einmal objekt erstellt?



  • folgender schreibfehler: verdrehung des referenzzeichens im kopierkonstruktor, ist mir unterlaufen.
    und das visual studio 6 gibt mir einen lustigen bericht (der gcc wirft dagegen an der korrekten stelle einen fehler):

    #include <iostream>
    #include <string>
    using namespace std;
    
    struct C { 
    
    	char * str;
    
    	C(){
    		str="hey du";
    		cout<<"ich lebe"<<endl;
    	}
    
    	C(const & C ){	//interpretiert ev. ein int, aber was passiert hier intern? 
    
    		cout<<"verschriebener kopierer"<<endl;
    	}
    
    	~C(){
    		cout<<"bin tot"<<endl; 
    	}
    
    /*	
    	C(const C &){
    		cout<<"kopiert richtig"<<endl;
    	}*/
    };
    
    void g() { 
    	throw C(); 
    }
    
    void f() { 
    
    	try { 
    
    		g(); 
    	} catch ( C & err) { 
    
    		cout << err.str<<endl; 
    	} 
    
    	cout<<"nach try catch"<<endl;
    }
    
    int main()
    {
    	f();
    
    	cout<<"the end"<<endl;	
    }
    

    ausgabe:

    ich lebe
    bin tot
    hey du
    bin tot
    nach try catch
    the end

    nur ein konstruktoraufruf, zwei destruktoraufrufe.

    bei richtigem kopierkonstruktor erfolgt die erwartete ausgabe:

    ich lebe
    hey du
    bin tot
    nach try catch
    the end

    ich habe den test gemacht, und den richtigen kopierkonstruktor parallel geschaltet, um zu sehen, ob er eine kopie erzeugt.
    er erzeugt keine kopie, aber wie durch ein wunder ist der fehler dann weg, es kommt die richtige ausgabe mit einem konstruktoraufruf und einem destruktor.

    irgendwie seltsam..
    übersehe ich was?



  • Das gleiche Verhalten habe ich bei mir auch (VC6 SP6). Allerdings scheint es, dass VC genau dann nur einmal den Destruktor aufruft, wenn irgendein Teil der Klasse (Membervariable, die Klasse selbst, Basisklasse) einen Kopierkonstruktor besitzt.

    Wenn ich das erstellte Assemblerlisting richtig deute, wird das Objekt in der Variante ohne eigenen Kopierkonstruktor aber tatsächlich kopiert, und die Adresse von this ist bei beiden Destruktoraufrufen verschieden. Insofern müsste das Programm korrekt funktionieren.
    Dort heißt es, es könnte eine Optimierung sein. Die Indizien deuten auch darauf hin, aber sonst finde ich nichts konkretes.

    Der Tippfehler beim anderen Konstruktor hat anscheinend gar keine Auswirkungen. VC6 müsste ihn als C(const int& C) übersetzen (wobei die "implicit int"-Regel soweit ich weiß in C++ eigentlich abgeschafft wurde).



  • tag schrieb:

    Der Tippfehler beim anderen Konstruktor hat anscheinend gar keine Auswirkungen.

    Yep, scheint mir auch so. Der aktuelle MSC lässt dies übrigens auch nicht mehr zu und gibt eine Fehlermeldung aus.
    Das Verhalten ist jedenfalls echt witzig, lässt sich aber relativ einfach erklären. Du siehst 2 mal den dtor und einmal den (default) ctor. Was du nicht siehst, ist der vom Compiler selbst generierte copy-ctor, denn, wie tag schon sagte, wird

    C(const & C )
    

    zu

    C(const int& C )
    

    Wenn man nun den copy-ctor selbst definiert, ist auch die Ausgabe völlig ok. Seltsamerweise hab ich dann nur noch einmal (default) ctor und einmal dtor (was ja eigentlich völlig ok ist).



  • thanks for bestätigung

    dacht schon, ich werde leicht senil...

    🙂



  • Mal ein Gedanke am Rande: Wo lebt denn so ein Exception-Objekt überhaupt?

    In g wird ein C erzeugt und geworfen. Dann wird der Stack von g geräumt, dabei wird das C nicht zerstört, also kann es nicht in g liegen. Mein Verständnis der C++-Semantik ist jedoch, dass es keinen Unterschied zwischen anonymen und benannten Objekten gibt, so dass

    throw C();
    

    und

    C someC; throw someC;
    

    identisch sind, und somit das Exception-Objekt doch auf dem lokalen Stack liegt. Gibt es bei throw eine Sonderregel? Würde in letzterem Beispiel eine Kopie erzeugt?



  • erstmal lese ich folgendes:

    Grob gesprochen: Die „Funktion“ throw ruft sozusagen die „Funktion“ catch auf.
    throw nimmt seine Argumente stets by value, d.h. arbeitet mit einer Kopie, einem
    temporären Objekt. Das ist wichtig, da das ursprüngliche Argument ja vermutlich
    beim stack unwinding zerstört worden ist.

    deswegen ist die konstruktion, ihm throw C(); zu übergeben, die geeignetete.
    aber wo es hinkommt, finde ich nicht.



  • elise schrieb:

    erstmal lese ich folgendes:

    Grob gesprochen: Die „Funktion“ throw ruft sozusagen die „Funktion“ catch auf.
    throw nimmt seine Argumente stets by value, d.h. arbeitet mit einer Kopie, einem
    temporären Objekt. Das ist wichtig, da das ursprüngliche Argument ja vermutlich
    beim stack unwinding zerstört worden ist.

    Das kann nicht sein, wie dein Beispiel (mit selbst definiertem CopyCtor) zeigt, wird nur eine einzige Instanz erstellt und nach dem catch zerstört.



  • na ja, ich mache dort ja keine copie.

    so wird der copykonstruktor aufgerufen:

    struct C { 
    
    	char * str;
    
    	C(){
    		str="hey du";
    		cout<<"ich lebe"<<endl;
    	}
    
    	~C(){
    		cout<<"bin tot"<<endl; 
    	}
    
    	C(const C &){
    		str="hey du";
    		cout<<"kopiert richtig"<<endl;
    	}
    };
    
    void g() { 
    	C my;
    	throw my;
    }
    
    void f() { 
    	try { 
    		g(); 
    	} catch ( C & err) { 
    		cout << err.str<<endl; 
    	} 
    	cout<<"nach try catch"<<endl;
    }
    
    int main(){
    	f();
    	cout<<"the end"<<endl;	
    }
    

    ich lebe
    kopiert richtig
    bin tot
    hey du
    bin tot
    nach try catch
    the end

    ps: trotz der referenz legt er eine kopie an.



  • Hallo,
    beim Werfen einer Exception wird immer nur eine Kopie des eigentlichen Exception-Objekts verwendet. Wo genau diese Kopie liegt, ist dem Compiler überlassen. Er muss nur garantieren, dass diese bis zum letzten catch-Block gültig bleibt (und das er sie mit throw; immer wieder referenzieren kann).

    Wenn bei throw C();
    kein Copy-Ctor aufgerufen wird, heißt dass nur, dass eine zu RVO identische Optimierung angewendet wurde, sprich: statt ein C zu konstruieren und dieses dann in die Exception-Objekt-Lokation zu kopieren wird das C-Objekt einfach direkt dort konstruiert. Das selbe kann der Compiler auch bei
    C localC;
    throw localC;
    machen, was dann der NRVO entspricht.
    Sehr gut erkennen kann man dieses Verhalten an einem standardkonformen Compiler.
    Beispiel:

    class C
    {
    public:
         C();
    private:
        C(const C&);
    };
    
    void f()
    {
    throw C();
    }
    

    Wie man sieht ist der Copy-Ctor von C private. Der Standard schreibt nun aber vor, dass selbst wenn RVO angwendet wird, der Copy-Ctor trotzdem "accessible" sein muss.
    Der Comeau schreibt hier deshalb auch:

    "C::C(const C &)" is inaccessible
    (Even though the copy was eliminated, the standard
    still requires it to be accessible)

    Einige Compiler prüfen diese Bedingung aber nicht. Sie eliminieren hier den Copy-Ctor-Aufruf und sind glücklich.

    Meine eigentliche Aussage: throw erzeugt (konzeptionell) *immer* eine Kopie des zuwerfenden Exception-Objekts.



  • thx 🙂



  • HumeSikkins schrieb:

    zu RVO identische Optimierung

    Ah danke, das ergibt Sinn. 🙂


Anmelden zum Antworten