return by value - deep/shallow copy



  • Ich bin mir nicht ganz sicher ob ich dich richtig verstanden habe, aber ich glaube was du sagst ist korrekt.

    Als Beispiel folgender Code:

    class Foo
    {
    public:
    	int* i;
    	Foo()
    		: i( new int(10) )
    	{}
    
    	~Foo()
    	{
    		delete i;
    	}
    };
    
    Foo foo()
    {
    	Foo f;
    	return f;
    }
    
    int main() 
    { 
    	Foo cpy = foo();
    	cout << *cpy.i << endl; // Undefined behavior
    }
    

    Hier hast du das Problem, dass beim Verlassen der Funktion foo() das f kopiert wird und dann zerstoert. Dabei wird der Destruktor von f aufgerufen und delete auf i aufgerufen. Die Kopie ist eine Shallow-Copy, das heisst die Kopie, die zurueckgegeben wird, zeigt nun auf geloeschten Speicher. Das Dereferenzieren in Zeile 24 erzeugt dann undefiniertes Verhalten.

    *Edit
    Die Loesung waere dann natuerlich einen Copy-Constructor einzufuehren, der eine Deep Copy erstellt:

    Foo( const Foo& f )
    		: i( new int(*f.i) )
    	{}
    

    Oder besser: Gleich RAII verwenden oder gar nicht erst new brauchen.



  • Ja vielen Dank, genau das meinte ich 🙂

    Könnte man das Problem mit Smart-Pointern lösen?
    Wenn ich das richtig verstanden habe löschen diese sich selbst, sobald keine Referenz mehr auf den entsprechenden Speicher existiert, was in unserem Fall bedeuten würde, dass nach verlassen von foo() der Speicher auf die 10 ints nicht freigegeben wird, da der Rückgabewert von foo() noch eine Referenz auf diesen beinhaltet.

    Gruß 🙂



  • Ja man kann dies mit Smart Pointern loesen. Smart Pointer sind eine Moeglichkeit RAII umzusetzen.

    Es gibt allerdings ganz verschiedene Smart Pointer. Es funktionieren nicht alle ueber Reference Count. Ich denke fuer diesen Fall waere ein shared_ptr angebracht.

    Hier werden ein paar Smart Pointer aus der boost library erklaert. shared_ptr ist im namespace tr1 und wird von den modernen Compilern unterstuezt.



  • Vielen Dank für deine Hilfen 🙂



  • Smartpointer bzw. RAII und das Problem der Tiefen/Flachen-Kopien sind orthogonal zueinander.
    Auch wenn Du einen shared_ptr benutzt, hast Du nach wie vor nur flache Kopien.



  • Danke 🙂

    Ich meinte bzgl. der smart Pointer jetzt nur Erstellen/Zerstören der Speicherbereiche und nicht die Modifikation, habe ich vergessen beizuschreiben

    Gruß



  • Ne, nix. Mit C++11 Move-Konstruktoren ist das Problem doch längst gelöst, wundert mich, dass das noch niemand erwähnt hat. oO



  • Danke für deine Antwort 🙂

    Kann man es denn irgendwie bewerkstelligen, dass bei der return-Anweisung gemoved wird anstatt kopiert, d.h. dass der move-Konstruktor verwendet wird anstatt der copy-Konstruktor?



  • Das passiert automatisch, sofern ein Move-Konstruktor vorhanden ist.



  • Kann man dann zusammenfassend festhalten, dass wenn man eine lokale Variable returned automatisch der move-Konstruktor gewählt wird, sofern er vorhanden ist (ansonsten der copy-Konstruktor) und wenn eine globale Variable returned wird stets der copy-Konstruktor?

    Gruß 🙂



  • Ne, es gibt auch noch statische Variablen etc.
    Es wird immer kopiert, es sei denn durch das return statement wird die Lebenszeit zurückgegebenen Objekts beendet, dann wird gemoved. Irgendwie logisch. 😉



  • Ja stimmt 🙂

    Vielen Dank 🙂



  • Nein das stimmt so nicht. Die Ausgabe dieses Programms

    #include <iostream>
    
    class A
    {
    public:
        A()
        {
            std::cout << "default" << std::endl;
        }
        A( const A& )
        {
            std::cout << "copy" << std::endl;
        }
        A( A&& )
        {
            std::cout << "move" << std::endl;
        }
        ~A()
        {
            std::cout << "destruction" << std::endl;
        }
    };
    
    A foo()
    {
        A a;
        return a;
    }
    
    int main()
    {
        A a = foo();
    }
    

    Ist bei mir beispielsweise nur

    default
    destruction
    

    Der compiler konstruiert das A aus foo direkt dorthin, wo es nach der Funktion gebraucht wird, d.h. es wird nie kopiert. Stattdessen übergibt main() foo() implizit einen return_ptr (oder wie man das auch nennen will), wohin das Objekt konstruiert werden soll, d.h. auf den Stackbereich von main(). Dadurch kann foo() einfach returnen und braucht a gar nicht zu kopieren oder zu moven.
    Hier ist sowas nicht möglich, da je nach Pfad ein anderes Objekt returned werden kann (zumindest moved mein gcc hier):

    A foo()
    {
        A a;
        A b;
        if( true )
            return a;
        else
            return b;
    }
    

    Ich meine diese Optimierung heißt 'return value optimization', danach kannst du mal googlen. Es gibt auch einige gute Tutorials zum Thema move, z.B. 'moving forward'.



  • Danke für deine Hilfe, bekommt man diese Ausgabe auch, wenn man alle Optimierungen deaktiviert?



  • Nein, RVO wird üblicherweise nur im Releasemodus durchgeführt. Du bekommst also im Debugmodus eine andere Ausgabe.



  • Bei mir schon, ja. Ich weiß aber nicht, ob diese Optimierung verpflichtend im Standard festgelegt ist, oder ob man auch standardkonform immer kopieren/moven darf.



  • Sie wird wohl nicht verpflichtend sein, wenn im Debugmodus die "normale" Ausgabe kommt. 😉



  • Der Debug-Modus wird beim GCC mit -g eingeleitet oder? Bei mir ändert sich da nichts an der Ausgabe 😞



  • Kann sein, dass GCC das immer an hat.



  • Ich bekomme aber die Ausgabe (mit oder ohne -g), die eigentlich im Release-Modus erscheinen sollte:

    default
    destruction


Anmelden zum Antworten