Copy-Constructor und operator*



  • hustbaer schrieb:

    ...aber egal.

    Finde ich jetzt nicht .... immerhin schien camper eine Lösung zu haben (die er leider noch nicht aufgedeckt hat).
    Außerdem sollte RVO doch Seiteneffekte erkennen/berücksichtigen .. das tut sie aber im vorliegenden Fall nicht (habe mal spaßeshalber einen CopyCtor mit Seiteneffekt gebastelt und der wird auch nicht aufgerufen).
    Und schließlich ist es IMO kein sooo exotischer Fall, dass man sowieso nicht in diese Falle tappen könnte.

    Wäre schon schön, wenn hier klar würde, wo der (Denk-)fehler liegt.

    Gruß,

    Simon2.



  • Ich nehme an RVO ist eine selbstverständliche Optimierung und keine, die man je nach Bedarf einschalten können soll (wäre imo auch logisch gesehen gut).

    Was das Beispiel angeht, hat Simon2 ja schon gemutmaßt, dass es sich um RVO handelt. Hier wird das Objekt tmp (welches im operator* erzeugt und zurückgegeben wird) direkt in c hineinkonstruiert, es wird also kein Copy-Ctor benötigt.

    Dummy c = a*b; // äquivalent zu
    Dummy c( a*b ); // äquivalent zu
    Dummy c( a.operator*( b ) );
    
    // Pseudocode für operator*
    Dummy operator*( Dummy const& left, Dummy const& right ) 
        returns: Dummy tmp;
    {
    }
    
    // Ablauf nach RVO:
    Dummy c; 
    operator*( c, a, b );
    void operator*( Dummy& target, Dummy const& left, Dummy const& right )
    {
    }
    

    Dummy "c" ist also Dummy "tmp", weshalb nirgendwo etwas kopiert werden muss.

    Bei der Rückgabe einer (konstanten) Referenz hingegen sollte klar sein, dass die Möglichkeit nicht besteht, die Initialisierung des Objekts in den Operator zu verlegen, da eine Rückgabe einer Referenz auf ein lokales Objekt undefiniert wäre. Demnach muss das zurückgegebene Objekt ausserhalb der Funktion existieren (this-Objekt, global, jedoch nicht im Aufrufer, es seidenn der Operator gibt einen der Operanden zurück ...), wodurch dessen Konstruktion nicht von der Funktion in den Aufrufer verlegt werden kann, wodurch eine Kopie erforderlich wird.

    Dummy c( a*b ); // Kopie erforderlich
    Dummy const& operator*( Dummy const& left, Dummy const& right )
    {
        return somethingNotLocal;
    }
    

    PS: Dieser Beitrag dient auch für mich selbst dazu, die Zusammenhänge auseinanderzuklamüsern. Sollte hier erwiesenermassen Quatsch stehen würde ich Euch (camper) bitten, das möglichst klarzustellen 😉



  • Hi,

    mir scheint's auch immer mehr RVO zu sein...

    allerdings bin ich ein wenig beunruhigt, weil es mein Verständnis von der Konstruierei doch ein wenig durcheinanderwirbelt.

    Letztlich bleibt:

    dummy f() { ... };
    
    int main() {
       dummy a = f();
    ...
    

    In dem Fall bestimmt nicht "der Erzeuger von a", welcher Konstruktor aufgerufen wird (der evtl. erwartete CopyCtor), sondern der Implementierer von f().
    Das beschränkt ein wenig die Möglichkeiten des dummy-Entwicklers, weil hier RVO wohl letztlich von einer "Standard-Copy-Konstruktion" (also Objekte sind hinterher im Speicher identisch) ausgeht und irgendwelche exotischen Seiteneffekte (fällt mir gerade kein Beispiel ein; Referenzcounting würde sich daran ja nicht stören) nicht einbauen kann.
    Naja, immerhin kann er das Kopieren noch ganz verbieten ...

    Hmmm strange....

    Gruß,

    Simon2.



  • Sieh es doch mal so.Die Compiler sind jetzt so intelligent, dass sie Dinge optimieren an die du nie denken würdest, egal ob du das so willst oder nicht. 🙂
    Ich hatte den Test auch gemacht und mich schon gewundert, das der g++ im debugmodus den CopyCTor rausoptimiert. Sobald im Operator* etwas vernünftiges stand war er aber wieder drin.



  • Hi,

    was war denn "etwas Vernünftiges" ?

    Gruß,

    Simon2.



  • dummy operator*( const dummy & other )
    {
        //code
        cout << "Operator*" << endl;
        dummy tmp(other); //lokales Obj für Matrixmultiplikation
                    // mache multiplikation
        return tmp;
    };
    


  • Braunstein schrieb:

    dummy operator*( const dummy & other )
    {
        //code
        cout << "Operator*" << endl;
        dummy tmp(other); //lokales Obj für Matrixmultiplikation
                    // mache multiplikation
        return tmp;
    };
    

    Naja, den CopyCtor explizit aufrufen, kann ja jeder.... 😉
    ... und RVO findet ja trotzdem statt.

    Mein "Vernünftiges" sah nämlich so aus:

    class dummy {
       int i;
    // ...
    };
    
    dummy operator*( const dummy & other )
    {
        //code
        cout << "Operator*" << endl;
        dummy tmp;
        tmp.i = i * other.i;
        return tmp;
    };
    

    .... und da wird KEIN CopyCtor aufgerufen, obwohl es mir als sehr vernünftige Implementation dieses operators erschien... 😃

    Gruß,

    Simon2.



  • Das sehe ich auch so. ....... jetzt! 😃
    Obwohl die Verwendung des CopyCtor hier den weiteren Aufruf eines Default-Konstrukltors bedingen würde (für das temporäre Objekt). Sonst gäbe es ja nichts zum kopieren. Und genau das scheint der Compiler ja wegzuoptimieren.
    Ich gebe zu, der direkte Aufruf des CopyCTor war kein so gutes Beispiel.



  • Simon2 schrieb:

    Außerdem sollte RVO doch Seiteneffekte erkennen/berücksichtigen ..

    Wäre mir neu. Wie kommst du darauf?
    RVO ist nur eine der ganzen Stellen wo der Programmierer nicht genau wissen kann wo bzw. wie oft seine Objekte rumkopiert werden...

    Ein Copy-Ctor muss - damit eine Klasse in C++ sinnvoll einsetzbar ist - immer so geschrieben sein dass

    Foo a = x;
    

    zu

    Foo tmp = x;
    Foo a = tmp;
    

    äquivalent ist -- also was x und a angeht.



  • Also ich verwende VS2005.NET Standardedition.
    Und wenn ich

    #include <iostream> 
    
    using namespace std; 
    
    class dummy 
    { 
        public: 
            dummy() 
            { 
                cout << "Default-Constructor" << endl; 
            }; 
            dummy( const dummy& other ) 
            { 
                cout << "Copy-Constructor" << endl; 
                            //mache deep copy 
            }; 
            dummy operator*( const dummy & other ) 
            { 
                //code 
                cout << "Operator*" << endl; 
                dummy tmp; //lokales Obj für Matrixmultiplikation 
                            // mache multiplikation 
                return tmp; 
            }; 
            dummy& operator=(const dummy& other) 
            { 
                cout << "Operator=" << endl; 
    
                            //erstelle deep-copy etc. 
    
                return *this; 
            }; 
    }; 
    
    int main() 
    { 
        dummy a; 
        dummy b; 
        dummy c = a * b; 
        return 0; 
    }
    

    mit den Standardeinstellungen compiliere, sieht die Ausgabe so aus:

    Default-Constructor
    Default-Constructor
    Operator*
    Default-Constructor
    Copy-Constructor

    Also bei mir wird der Copykonstruktor demnach aufgerufen oO.



  • Kahino schrieb:

    Also ich verwende VS2005.NET Standardedition.
    Und wenn ich [...] mit den Standardeinstellungen compiliere [wird bei mir] der Copykonstruktor ... aufgerufen oO.

    Hast Du im Debug- oder im Release-Modus kompiliert? Im Release-Modus ist der Standard für die Optimierungen in VS 2005 /O2, also Optimierung auf Geschwindigkeit. Bei Debug-Modus hingegen werden standardmäßig keine Optimierungen vorgenommen.



  • hustbaer schrieb:

    Simon2 schrieb:

    Außerdem sollte RVO doch Seiteneffekte erkennen/berücksichtigen ..

    Wäre mir neu. Wie kommst du darauf?...

    Intuition. 😃

    ... aber offensichtlich eine falsche.

    hustbaer schrieb:

    ...
    Ein Copy-Ctor muss - damit eine Klasse in C++ sinnvoll einsetzbar ist - immer so geschrieben sein dass

    Foo a = x;
    

    zu

    Foo tmp = x;
    Foo a = tmp;
    

    äquivalent ist -- also was x und a angeht.

    Jupp, genau darauf sind wir hier (habe mit Arbeitskollegen eine zeitlang darüber diskutiert) inzwischen auch gekommen.

    Letztlich hatte ich außerdem gedacht, dass RVO ein "Sprachfeature" sei und weniger eine "abschaltbare Optimierungsoption" ... .

    Das war mir eben neu.

    Danke,

    Simon2.



  • Konrad Rudolph schrieb:

    Kahino schrieb:

    Also ich verwende VS2005.NET Standardedition.
    Und wenn ich [...] mit den Standardeinstellungen compiliere [wird bei mir] der Copykonstruktor ... aufgerufen oO.

    Hast Du im Debug- oder im Release-Modus kompiliert? Im Release-Modus ist der Standard für die Optimierungen in VS 2005 /O2, also Optimierung auf Geschwindigkeit. Bei Debug-Modus hingegen werden standardmäßig keine Optimierungen vorgenommen.

    Ist das beim gcc eigentlich auch abschaltbar ?
    Noopt (-O0) habe ich eigentlich angeschaltet.

    Gruß,

    Simon2.


  • Mod

    RVO und allgemein die Auslassung des Copy-Ctors sind keine solchen Optimierungen, die man üblicherweise durch Compileroptionen beeinflussen kann. Bei letzteren geht es i.allg. um die Veränderungen von Parametern des Programmes, die durch den Standard nicht festgelegt werden (wie etwa die Dauer des Programmablaufs) oder jedenfalls weitgehend der Implementation vorbehalten sind (etwa das genaue Verhalten von Gleitkommaoperationen). Dem gegenüber verändert die Auslassung des Copy-Ctors das beobachtbare Verhalten i.S.d. Standards. Sie verändert also die Interpretation des Programmes. Es ist also tatsächlich - wenn man diese Einteilung wünscht - ein Merkmal der Sprache und keine Fähigkeit des Compilers an sich.

    Was brauchen wir für die Auslassung des Copy-Ctors?
    Wenn der Copy-Ctor ausgelassen wird, so nimmt das zu initialisierende Objekt die Identität des initialisierenden Ausdrucks an, im Gegenzug wird letzterer nicht (vorzeitig) zerstört. Die semantische Veränderung des Programmes darf aber nur im Nichtausführen eben des Copy-Ctors bzw. Destruktors bestehen. D.h. kann man zu irgendeinem Zeitpunkt sowohl auf den initialisierenden Ausdurck als auch auf das zu initialisierende Objekt gleichzeitig zugreufen, dann müssen diese logisch getrennt - und eben nicht als identisch im Objekt - erscheinen, die Auslassung des Copy-Ctors ist dann folglich ausgeschlossen. Schlussfolgerung:
    Der initialisierende Ausdruck muss stets ein temporäres Objekt sein. Zudem - das dürfte ohnehin klar sein - müssen die dynamischen Typen abgesehen von cv-Qualifikation (soweit diese cv-Qualifikation die Aufrufbarkeit des Copy-Ctors nicht berührt) identisch sein. Damit sind Referenzen (als Initialisier) in doppelter Hinsicht ausgeschlossen: Einerseits sind es keine temporären Objekte, andererseits gibt es für die Übereinstimmung des dynamischen Typs keine Garantie. Sind diese beiden Bedingungen erfüllt:
    - temporäres Objekt
    - identischer Typ
    kann der Copy-Ctor überall dort ausgelassen werden, wo der Standard das vorsieht. RVO ist hier im Grunde nur ein Spezialfall. NRVO ist eine Erweiterung, dort ist der initialisierende Ausdruck zwar kein temporäres Objekt, würde aber nach dem Return-Statement ohnehin zerstört werden.



  • RVO ist *erlaubt*, wird aber vom Standard nicht *gefordert*. Daher ist es IMHO eine Optimierung.
    @camper:
    Ich denke das "normale auslassen eines copy ctor" deckt aber folgenden Fall nicht ab:

    foo f()
    {
        foo r(123);
        return r;
    }
    

    r ist nicht temporär, also müsste es zumindest 1x kopiert werden. Wird es aber AFAIK oft nicht.

    p.S.: oder ist es das was du als "NRVO" bezeichnest? Steht das "N" für "named"? Kenne die Abkürzung nicht... ist die gebräuchlich?



  • hustbaer schrieb:

    r ist nicht temporär, also müsste es zumindest 1x kopiert werden. Wird es aber AFAIK oft nicht.

    p.S.: oder ist es das was du als "NRVO" bezeichnest?

    Wenn das optimiert wird, ist es NRVO. "N" steht für "named". Diese Abkürzung ist übrigens AFAIK gängiger als RVO, letztere ist eher informell.


  • Mod

    hustbaer schrieb:

    RVO ist *erlaubt*, wird aber vom Standard nicht *gefordert*. Daher ist es IMHO eine Optimierung.

    Und damit hast du sicher nicht unrecht, auch steht das O ja für Optimierung. Jedenfalls aber - und darauf kommt es mir an - verändert diese Optimierung das Verhalten des Programmes, und damit ist sie von ganz anderer Qualität als andere übliche Optimierungen.

    @camper:
    Ich denke das "normale auslassen eines copy ctor" deckt aber folgenden Fall nicht ab:

    foo f()
    {
        foo r(123);
        return r;
    }
    

    r ist nicht temporär, also müsste es zumindest 1x kopiert werden. Wird es aber AFAIK oft nicht.

    p.S.: oder ist es das was du als "NRVO" bezeichnest? Steht das "N" für "named"? Kenne die Abkürzung nicht... ist die gebräuchlich?

    Du hast es erkannt.

    RVO für die Rückgabe temporärer Objekte:

    foo f() { return foo(); }
    

    wurde vor NRVO eingeführt und ist auch leichter zu implementieren.



  • Hast Du im Debug- oder im Release-Modus kompiliert? Im Release-Modus ist der Standard für die Optimierungen in VS 2005 /O2, also Optimierung auf Geschwindigkeit. Bei Debug-Modus hingegen werden standardmäßig keine Optimierungen vorgenommen.

    Hast Recht, nachdem ich zu den Standardreleaseeinstellungen geschaltet hatte und das Programm compiliert habe, wurde der Kopierkonstruktor nicht aufgerufen.


Anmelden zum Antworten