Move-Semantik



  • Wie sollte der hier auch Sinn machen? Du möchtest das Objekt ja verändern und dann macht es keinen Sinn ein const-Objekt zurück zu liefern.

    Der Satz hätte vielleicht eher lauten sollen: "Da kein Copy-Konstruktor für A existieren kann, der const As akzeptiert...".
    Wie dem auch sei, ich gebe das const in diesem Fall ja nicht vor:

    A getA(){
       A a;
       return a;
    }
    A a = getA();    // Compilerfehler, falls A keinen Copy-Ctor für const A& besitzt
    

    Oder wie meintest du das?

    Edit: BTW, die null Abfrage vor dem delete ist überflüssig!

    Wusste garnicht, dass einem delete die Überprüfung schon abnimmt 😮

    Warum nimmst Du nicht std::auto_ptr<T>?

    Verschiebt dies das Problem nicht nur? Wenn ich anstatt des rohen Zeigers nun den auto_ptr aus der konstanten A-Referenz ziehen möchte, müsste ich doch den gleichen Fehler bekommen?

    Mach den ptr mutable.

    Das schaue ich mir gleich mal an.



  • Meine Kenntnisse über die Move-Geschichte ist vllt. schon etwas angestaubt, aber damals sah das so aus:

    struct A
    {
        A(A&& a);
    };
    

    Also zwei "&"!
    Dann kannst du weiterhin deinen Copy-CTor anbieten.
    Vllt. ist das ja der ganze Fehler.


  • Mod

    Wenn du schon rvalue-refernzen arbeiten kannst, ist das der sinnvollste Weg. Ansonsten kann ein auto_ptr-artiger Ansatz helfen:

    class A
    {
    private:
         int* ptr;
         class Aref
         {
             A& a;
         public:
             Aref(A& a) : a(a) {}
             int* move() { int* p = a.ptr; a.ptr = 0; return p; }
         };
         friend class Aref;
    public:
         A(A& a) : ptr( Aref( a ).move() ) {}
         A(Aref ref) : ptr( ref.move() ) {}
         ~A(){
              if(ptr!=0)
                   delete ptr;
         }
         A& operator=(A& a) { ptr = Aref( a ).move(); return *this; }
         A& operator=(Aref ref) { ptr = ref.move(); return *this; }
         operator Aref() { return Aref( *this ); }
    };
    

    Das Ganze funktioniert allerdings nicht, falls daneben noch normale Copy-Semantik angeboten werden muss, oder zusätzliche Konvertierungen zwischen As (man denke auto_ptr mit kompatiblen Typen) benötigt werden.



  • Dieser Artikel aus dem Jahr 2003 (pre-Rvalue-Referenzen) ist vielleicht nicht ganz uninteressant:
    "Move Constructors".



  • Beispiel eines einfachen "move-only"-Typs ohne destruktive Kopie und ohne Rvalue-Referenzen:

    #include <iostream>
    
    //-----[ simple unique_ptr emulation in C++03 ]-----
    
    template<class T>
    class unique_ptr
    {
    	T* ptr;
    	struct proxy { T* ptr; };
    	unique_ptr(unique_ptr&);     // non-const lvalues bind here
    	void operator=(unique_ptr&); // non-const lvalues bind here
    public:
    	explicit unique_ptr(T* p=0) : ptr(p) {}
    
    	~unique_ptr() {delete ptr;}
    
    	unique_ptr(proxy t) : ptr(t.ptr) {}
    
    	unique_ptr& operator=(proxy t)
    	{
    		delete ptr;
    		ptr = t.ptr;
    		return *this;
    	}
    
    	T* release()
    	{
    		T* r = ptr;
    		ptr = 0;
    		return r;
    	}
    
    	operator proxy() // non-const unique_ptr --> proxy
    	{
    		proxy r = {ptr};
    		ptr = 0;
    		return r;
    	}
    
    	friend unique_ptr move(unique_ptr& lvalue)
    	{ return unique_ptr(lvalue.release()); }
    
    	T& operator*() const {return *ptr;}
    	T* operator->() const {return ptr;}
    };
    
    //-----[ example program ]-----
    
    unique_ptr<int> source(int z)
    {
    	unique_ptr<int> p ( new int );
    	*p = z;
    	return move(p);
    }
    
    void sink(unique_ptr<int> p)
    {
    	std::cout << *p << '\n';
    }
    
    int main()
    {
    	sink(source(23));
    	unique_ptr<int> p = source(42);
    	p = source(99);
    	unique_ptr<int> q;
    	q = move(p);
    	sink(move(q));
    }
    

    An den Stellen, an denen move benutzt wird, geht es nicht ohne move ==> keine versehentlich destruktiven Kopien.

    Zumindest funktioniert das mit dem G++ Compiler.

    Gruß,
    SP


  • Mod

    Sebastian Pizer schrieb:

    Beispiel eines einfachen "move-only"-Typs ohne destruktive Kopie und ohne Rvalue-Referenzen:

    An den Stellen, an denen move benutzt wird, geht es nicht ohne move ==> keine versehentlich destruktiven Kopien.

    Das ist die gleiche Idee. Und so wie g++s auto_ptr ist es nicht exceptionsicher.

    Alexandreis Mojo-System ist interessant aber imo zu komplex und aufdringlich.



  • camper schrieb:

    Das ist die gleiche Idee.

    Je nachdem, was Du mit "Idee" meinst. 😉

    camper schrieb:

    Und so wie g++s auto_ptr ist es nicht exceptionsicher.

    Du hast Recht. Es ist nicht schön, den Zeiger im struct "ungeschützt" zwischen zu speichern. Das kann auch schon mal bei der Parameterübergabe an eine Funktion nach hinten losgehen -- denke ich.

    Ich war vorhin noch im Glauben, dass, wenn ich im struct einen Zeiger auf unique_ptr<T> statt T speichere, dass es zu "baumelnden Zeigern" kommen kann (dass *proxy.ptr eventuell ungültig wird). Ich weiß aber nicht mehr, wie ich zu dem Schluss gekommen bin. Es müsste auch so funktionieren:

    template<class T>
    class unique_ptr
    {
    	T* ptr;
    	struct proxy { unique_ptr<T>* pup; };
    	unique_ptr(unique_ptr&);     // non-const lvalues bind here
    	void operator=(unique_ptr&); // non-const lvalues bind here
    public:
    	explicit unique_ptr(T* p=0) : ptr(p) {}
    
    	~unique_ptr() {delete ptr;}
    
    	unique_ptr(proxy t) : ptr(t.pup->release()) {}
    
    	unique_ptr& operator=(proxy t)
    	{
    		delete ptr;
    		ptr = t.pup->release();
    		return *this;
    	}
    
    	T* release()
    	{
    		T* r = ptr;
    		ptr = 0;
    		return r;
    	}
    
    	operator proxy() // non-const unique_ptr --> proxy
    	{
    		proxy r = {this};
    		return r;
    	}
    
    	friend unique_ptr move(unique_ptr& lvalue)
    	{ return unique_ptr(lvalue.release()); }
    
    	friend void swap(unique_ptr & p1, unique_ptr & p2)
    	{
    		T* tmp = p1.ptr;
    		p1.ptr = p2.ptr;
    		p2.ptr = tmp;
    	}
    
    	T& operator*() const {return *ptr;}
    	T* operator->() const {return ptr;}
    };
    

    (wobei "proxy" jetzt eher die Funktion einer RValue-Referenz übernimmt)

    Sieht jemand ein mögliches Problem (mal davon abgesehen, dass ich auf benutzerdefinierte "Deleter" und Konvertierungen verzichtet habe)? Ich sehe im Moment keins.

    Gruß,
    SP


  • Mod


  • Mod

    Ich bin nicht sicher ob ich move in dieser Form mag, dem Zweck der Funktion nach, wird sie häufig eingesetzt werden, zum Beispiel Funktionsparameter zu initialisieren. Tritt nun nach Ausführen des move und vor Aufruf der anderen Funktion eine Exception auf, besteht zwar kein Leck mehr, aber das Objekt, auf das gezeigt wurde ist weg, das eigentliche Ziel wurde nie initialisert und der ursprüngliche Zeiger ist leer. Imo ist es zweckmäßiger, move ebenfalls einen proxy zurückgeben zu lassen. Dann muss man nur entsprechend bzgl. Selbstmove aufpassen:

    template<class T>
    class unique_ptr
    {
        T* ptr;
        struct proxy { unique_ptr<T>* ptr; };
        unique_ptr(unique_ptr&);     // non-const lvalues bind here
        void operator=(unique_ptr&); // non-const lvalues bind here
    public:
        explicit unique_ptr(T* p=0) : ptr(p) {}
    
        ~unique_ptr() {delete ptr;}
    
        unique_ptr(proxy t) : ptr(t.ptr->release()) {}
    
        unique_ptr& operator=(proxy t)
        {
            T* p = t.ptr->release();
            delete ptr;
            ptr = p;
            return *this;
        }
    
        T* release()
        {
            T* r = ptr;
            ptr = 0;
            return r;
        }
    
        operator proxy() // non-const unique_ptr --> proxy
        {
            proxy r = {this};
            return r;
        }
    
        friend proxy move(unique_ptr& lvalue)
        { return lvalue; }
    
        friend void swap(unique_ptr & p1, unique_ptr & p2)
        {
            T* tmp = p1.ptr;
            p1.ptr = p2.ptr;
            p2.ptr = tmp;
        }
    
        T& operator*() const {return *ptr;}
        T* operator->() const {return ptr;}
    };
    

  • Mod

    Ok, das hilft auch nicht. Die Exception könnte ja auch nach Initialisierung des Funktionsparameters auftreten, dann wäre der Objekt trotzdem weg.



  • camper schrieb:

    Ich bin nicht sicher ob ich move in dieser Form mag, dem Zweck der Funktion nach, wird sie häufig eingesetzt werden, zum Beispiel Funktionsparameter zu initialisieren. Tritt nun nach Ausführen des move und vor Aufruf der anderen Funktion eine Exception auf, besteht zwar kein Leck mehr, aber das Objekt, auf das gezeigt wurde ist weg, das eigentliche Ziel wurde nie initialisert und der ursprüngliche Zeiger ist leer.

    Das passiert genauso auch bei dem "echten unique_ptr", oder nicht?

    camper schrieb:

    Imo ist es zweckmäßiger, move ebenfalls einen proxy zurückgeben zu lassen.

    Das war sogar ursprünglich mein Plan -- wird aber vom G++ nicht mehr akzeptiert in einer Situation wie dieser:

    unique_ptr<int> q = move(p);
    

    oder dieser

    return move(p);
    

    wahrscheinlich, weil es zu viele Konvertierungen wären (proxy -> unique_ptr -> proxy -> unique_ptr) oder so ähnlich. Bind mir auch gar nicht sicher, ob ich mit meiner letzten Version einfach "Glück" hatte (gnädiger G++), oder ob's vom Standard her auch wirklich so funktionieren muss.

    Gruß,
    SP


  • Mod

    Sebastian Pizer schrieb:

    camper schrieb:

    Imo ist es zweckmäßiger, move ebenfalls einen proxy zurückgeben zu lassen.

    Das war sogar ursprünglich mein Plan -- wird aber vom G++ nicht mehr akzeptiert in einer Situation wie dieser:

    unique_ptr<int> q = move(p);
    

    oder dieser

    return move(p);
    

    wahrscheinlich, weil es zu viele Konvertierungen wären (proxy -> unique_ptr -> proxy -> unique_ptr) oder so ähnlich. Bind mir auch gar nicht sicher, ob ich mit meiner letzten Version einfach "Glück" hatte (gnädiger G++), oder ob's vom Standard her auch wirklich so funktionieren muss.

    Gruß,
    SP

    Weil der Zugriff auf den Copyctor von unique_ptrn icht möglich ist. Stimmt, hatte ich nicht bedacht - bisher habe ich nur mit auto_ptr-Implementationen gespielt, dort besteht das Problem nicht.



  • Moin,
    vielen Dank für die ganzen Antworten. Allerdings muss ich das Ganze erst einmal verdauen 😉

    camper schrieb:

    template<class T>
    class unique_ptr
    {
        T* ptr;
        struct proxy { unique_ptr<T>* ptr; };
        unique_ptr(unique_ptr&);     // non-const lvalues bind here
        void operator=(unique_ptr&); // non-const lvalues bind here
    public:
        explicit unique_ptr(T* p=0) : ptr(p) {}
    
        ~unique_ptr() {delete ptr;}
    
        unique_ptr(proxy t) : ptr(t.ptr->release()) {}
    
        unique_ptr& operator=(proxy t)
        {
            T* p = t.ptr->release();
            delete ptr;
            ptr = p;
            return *this;
        }
    
        T* release()
        {
            T* r = ptr;
            ptr = 0;
            return r;
        }
    
        operator proxy() // non-const unique_ptr --> proxy
        {
            proxy r = {this};
            return r;
        }
    
        friend proxy move(unique_ptr& lvalue)
        { return lvalue; }
    
        friend void swap(unique_ptr & p1, unique_ptr & p2)
        {
            T* tmp = p1.ptr;
            p1.ptr = p2.ptr;
            p2.ptr = tmp;
        }
    
        T& operator*() const {return *ptr;}
        T* operator->() const {return ptr;}
    };
    

    Der "Trick" mit dem Proxy ist mir neu, deswegen an dieser Stelle eine Frage dazu:
    Soweit ich das richtig sehe, erwartet die Klasse für den Copy-Ctor und operator= standardmäßig eine non-const Referenz auf einen anderen unique-Pointer. Für den Fall, dass dennoch eine const-Referenz von unique_ptr übergeben wird, findet zuerst eine Konvertierung von const unique_ptr->proxy statt und diesen Proxy kann ich anschließend problemlos modifizieren. Wieso funktioniert diese Konvertierung?

    Und was bedeuten die Klammern um this in

    operator proxy() // non-const unique_ptr --> proxy
        {
            proxy r = {this};
            return r;
        }
    

    krümelkacker schrieb:

    Dieser Artikel aus dem Jahr 2003 (pre-Rvalue-Referenzen) ist vielleicht nicht ganz uninteressant:
    "Move Constructors".

    Vielen Dank dafür. Werde ich mir mal in Ruhe zu Gemüte führen.


  • Mod

    Matzer schrieb:

    Soweit ich das richtig sehe, erwartet die Klasse für den Copy-Ctor und operator= standardmäßig eine non-const Referenz auf einen anderen unique-Pointer.

    Richtig. Und weil diese Member private und sowieso ohne Implementation sind, wird jeder Versuch, einen normalen non-const unique_ptr zu kopieren mit einem Fehler bestraft.

    Matzer schrieb:

    Für den Fall, dass dennoch eine const-Referenz von unique_ptr übergeben wird, findet zuerst eine Konvertierung von const unique_ptr->proxy statt und diesen Proxy kann ich anschließend problemlos modifizieren. Wieso funktioniert diese Konvertierung?

    Keineswegs. Da die für die Konvertierung zuständige Operationfunktion keine const-Memberfunktion ist, kann für const-unique_ptr kein Proxy erstellt werden. Hier erfolgt also auch eine Fehlermeldung durch den Compiler.
    Die einzige Art unique_ptr, die kopiert wreden kann, sind non-const rvalues - also im Regelfall Rückgabewerte von Funktionen. Rvalues (temporäre Objekte) binden nicht an non-const Referenzen, weshalb copy-ctor und copy-Zuweisung keine Kandidaten sind.



  • Die einzige Art unique_ptr, die kopiert wreden kann, sind non-const rvalues - also im Regelfall Rückgabewerte von Funktionen. Rvalues (temporäre Objekte) binden nicht an non-const Referenzen, weshalb copy-ctor und copy-Zuweisung keine Kandidaten sind.

    Ich bezog mich auch auf die Rückgabewerte von Funktionen. Nur verstehe ich gerade nicht, worin der Unterschied zwischen dem Binden des Rückgabewertes einer Funktion und einer Temporary besteht. Ich dachte folgendes wäre äquivalent:

    // Sei A irgendeine Klasse
    A getA(){
      A a;
      return a;
    }
    
    A oneA     = getA();  
    A anotherA = A();
    

    da nach der Auswertung von getA() auf der rechten Seite im Grunde auch nur eine Temporary stehen würde?



  • Matzer schrieb:

    camper schrieb:

    template<class T>
    class unique_ptr
    {
        T* ptr;
        struct proxy { unique_ptr<T>* ptr; };
        unique_ptr(unique_ptr&);     // non-const lvalues bind here
        void operator=(unique_ptr&); // non-const lvalues bind here
    public:
    
        [...]
    
        friend proxy move(unique_ptr& lvalue)
        { return lvalue; }
    

    Ich wollte nur nochmal darauf hinweisen, dass diese Abänderung von move dazu führt, dass keine Kopierinitialisierungen mehr möglich sind und damit auch return move(...); oder die Übergabe von move(...) an eine Funktion nicht mehr klappt.

    Matzer schrieb:

    Wieso funktioniert diese Konvertierung?

    Und was bedeuten die Klammern um this in

    operator proxy() // non-const unique_ptr --> proxy
        {
            proxy r = {this};
            return r;
        }
    

    Das ist ein Konvertierungsoperator, mit dem implizit unique_ptr-Objekte zu proxy-Objekten konvertiert können. Die geschweiften Klammern um this sind einfach Teil der Aggregats-Initialisierungs-Syntax. Der Typ proxy ist ein Aggreratstyp und der Zeiger r.pup wird hier durch this initialisiert.

    Mal ein paar Beispiele, wie das alles funktioniert:

    unique_ptr<int> foo = (initialisierer);
    
    Sei der Initialisierer ein cv-qualifizierter Ausdruck vom Typ
    unique_ptr<int>. Dann gibt es noch 4 Möglichkeiten: Alle
    Kombinationen aus const/non-const und LValue/RValue-Ausdruck.
    
    Initialisierer: | Kopierkonstruktor | Konvertierungskonstruktor
                    | kommt in Frage    | (proxy->unique_ptr)
    ----------------+-------------------+--------------
      const lvalue  |     nein (1)      |    nein (4)
      const rvalue  |     nein (1)      |    nein (4)
            lvalue  |       JA (2)      |      ja (5)
            rvalue  |     nein (3)      |      JA (6)
    
    1: non-const Referenz nicht mit const Ausdruck initialisierbar
    2: kommt in Frage und ist auch ein besserer Match als 5, führt
       aber zu einem Compilier-Fehler, da privat
    3: non-const Referenz nicht mit RValue Ausdruck ("temporäres
       Objekt") initialisierbar.
    4: const unique_ptr lässt sich nicht in proxy konvertieren, da
       der Konvertierungsoperator nicht const ist.
    5: kommt in Frage, ist aber ein schlechterer Match als 2
    6: temporärer non-const unique_ptr kann zu proxy hin- und wieder
       zurück konvertiert werden.
    

    unique_ptr ist ein "move only"-Typ. Richtig kopierbare Typen mit move-Konstruktor-Optimierung könnte man jetzt noch bekommen, in dem man folgende Konstruktoren und Zuweisungsoperatoren definiert:

    using boost::enable_if;
    using boost::is_same;
    
    class myclass
    {
      private:
        struct rval_ref { myclass& ref; rval_ref(myclass*p):ref(*p){} };
      public:
        // copy from const lvalues
        template<typename U>
        myclass(U &,
           typename enable_if<
              is_same<U,const myclass>
           >::type* =0);
    
        // copy from non-const lvalues
        myclass(myclass&);
    
        // move from non-const rvalue
        myclass(rval_ref);
    
        // copy assign from const lvalues
        template<typename U>
        myclass& operator=(U &,
           typename enable_if<
              is_same<U,const myclass>
           >::type* =0);
    
        // copy assign from non-const lvalue
        myclass& operator=(myclass&);
    
        // move assign from non-const rvalue
        myclass& operator=(rval_ref);
    
        operator rval_ref()
        { return rval_ref(this); }
    
        friend myclass move(myclass& lvalue)
        { return myclass(rval_ref(&lvalue)); }
    
        ...
    };
    

    (ungetestet), wobei der Trick mit den Templates dazu führt, dass konstante Objekte kopiert werden können, aber non-const temporäre Objekte immer noch über den rval_ref-Umweg "kopiert" werden.

    Nachteil dieser Emulationen gegenüber dem, was C++0x bieten wird:

    • Keine implizit konvertierenden Move-Operationen möglich (zB unique_ptr<Derived> --> unique_ptr<Base>. Das muss stattdessen über Hilfsfunktionen erledigt werden.
    • Objekte ziehen immer mehrfach um, keine move/copy elision. Bei unique_ptr<int> q = move(p); werden zwei temporäre Objekte und 3mal der "move ctor" aufgerufen (zumindest unter G++).

    Gruß,
    SP



  • Matzer schrieb:

    Ich dachte folgendes wäre äquivalent:

    // Sei A irgendeine Klasse
    A getA(){
      A a;
      return a;
    }
    
    A oneA     = getA();  
    A anotherA = A();
    

    da nach der Auswertung von getA() auf der rechten Seite im Grunde auch nur eine Temporary stehen würde?

    Richtig. In beiden Fällen handelt es sich auf der rechten Seite um einen "RValue-Ausdruck". Wenn Du "A()" in der zweiten Zeile durch "oneA" ersetzt, steht auf der rechten Seite ein "LValue-Ausdruck". Das ist ein Unterschied bzgl Optimierungen oder "move-only"-Typen wie unique_ptr. Ein unique_ptr-Objekt soll ja nicht kopiert werden können. Man will keine destruktive Kopie (Kopie, die das Quellobjekt verändert). Und man will auch keine Doppellöschungen. Die Idee ist eben die, unique_ptr-Objekte "umziehen" zu lassen und das Quellobjekt nur dann zu "zerstören" (den Zeiger darin auf 0 zu setzen), falls es keiner merkt (temporäres Objekt) oder falls es explizit erwünscht wurde (move(...)-Syntax).


Anmelden zum Antworten