Move-Semantik
-
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
movebenutzt wird, geht es nicht ohnemove==> keine versehentlich destruktiven Kopien.Zumindest funktioniert das mit dem G++ Compiler.
Gruß,
SP
-
Sebastian Pizer schrieb:
Beispiel eines einfachen "move-only"-Typs ohne destruktive Kopie und ohne Rvalue-Referenzen:
An den Stellen, an denen
movebenutzt wird, geht es nicht ohnemove==> 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
-
-
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;} };
-
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
-
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ß,
SPWeil 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.
-
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
movedazu führt, dass keine Kopierinitialisierungen mehr möglich sind und damit auchreturn move(...);oder die Übergabe vonmove(...)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
thissind einfach Teil der Aggregats-Initialisierungs-Syntax. Der Typ proxy ist ein Aggreratstyp und der Zeigerr.pupwird hier durchthisinitialisiert.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).