Anfänger std::move
-
Siassei schrieb:
Ist das soweit korrekt?
Nein, da du hier eine Referenz auf eine lokale Variable zurückgibst. RValue Referenzen sind auch nur Referenzen.
Siassei schrieb:
Wann benötige ich einen Move-Constructor in der Klasse?
Immer dann, wenn du deinen Typen moveable machen möchtest. Ein Beispiel wäre ein Socket, ein Thread oder ein Mutex, also typische Resourcen. Auch Files könnte man moveable machen.
Siassei schrieb:
Wann setzt man std::forward ein?
Immer dann, wenn du "perfect forwarding" haben möchtest. Aber ich denke das geht nun zu weit, auch wird man forward eher selten brauchen.
Da du Anfänger bist, würde ich dir empfehlen, von RValue Referenzen sowie std::move und std::forward erst mal die Finger zu lassen. Das ganze Zeug ist nicht einfach, wenn dann noch Templates hinzukommen wirds richtig lustig.
-
Siassei schrieb:
ich habe immer wieder mal ein Problem mit move aus C++0x.
class A { static A&& createA () { A a; std::move(a); }; } //---------- A a = A::createA ();Ist das soweit korrekt?
Ganz und gar nicht.
- Das return fehlt (U.B.)
- Selbst mit return würdest du eine Referenz auf ein lokales Objekt zurückgeben (U.B.)
- Selbst ohne Referenz ist das std::move dort eher schädlich (Du verhinderst damit Optimierungen, die viel besser sind als move-konstruktionen)Korrekt:
class A { public: static A createA() { A a; return a; }; }; //---------- A a = A::createA ();Wenn Du jetzt verhindern willst, dass das A-Objekt hier ggf kopiert wird, brauchst Du für A einen Move-Konstruktor.
Siassei schrieb:
Wann benötige ich einen Move-Constructor in der Klasse?
s.o.
Siassei schrieb:
Wann setzt man std::forward ein?
Typischerweise dann, wenn man "Perfect Forwarding" haben will. Manchmal aber auch so.
-
krümelkacker schrieb:
class A { static A createA() { A a; return a; }; }; //---------- A a = A::createA ();Wenn Du jetzt verhindern willst, dass das A-Objekt hier ggf kopiert wird, brauchst Du für A einen Move-Konstruktor.
Nur einen move-Constructor? Oder muss die Function nicht
static A&& createA() { A a; return a; };
heisen?Das Beispiel ist sehr einfach gehalten und ja, ich möchte das Kopieren verhindern.
z.B.
- ein Interface ist als Header-Only deklariert
- in einer DLL wird das Interface deklariert
- in einer weiteren DL (DynamicLibrary) wird nun die Instance für das Interface angefordert.
Ich kann mir nicht vorstellen, dass der Compiler über DL-Grenzen hinweg optimiert. Zudem möchte ich nun endlich verstehen, für was alles man das ganze Einsetzen kann und bin eben auf diesen Anwendungsfall gestossen.@Pi
Alles was C++0x betrifft bin ich absoluter Anfänger. Beim Rest würde ich mich nicht als solches Bezeichnen
Gruß, Thomas
-
Siassei schrieb:
Oder muss die Function nicht
static A&& createA() { A a; return a; };
heisen?Nein. Man gibt keine Referenzen auf funktionslokale Objekte zurück. Das war früher schon so und so ist es immer noch. (Ich wiederhole mich)
Siassei schrieb:
Das Beispiel ist sehr einfach gehalten und ja, ich möchte das Kopieren verhindern.
Dann spendiere Deiner Klasse einen move-ctor. (Ich wiederhole mich)
Siassei schrieb:
[...Interface-Gedöns...Compiler-Optimierung...]
WTF?!
Lies bitte die verlinkten Artikel.
-
Nur einen move-Constructor? Oder muss die Function nicht
static A&& createA() { A a; return a; };
heisen?Nur den Movekonstruktor und als Rückgabewert einfach nur "A".
Ich hoffe ich irre mich nicht, aber std::move braucht man eigentlich nur, um Variablen frühzeitig zu moven.
Im FallA createA() {A a; return a;}weiß der Compiler ja, dass das Objektanach der return-Anweisung in der Funktion nicht mehr gebraucht wird - deshalb kann er es in das "Rückgabeobjekt" rein-moven.Ein sinnvoller Anwendungsfall für std::move wäre aber z.B.:
void doSomething( foo Foo ); int main() { foo Objekt; doSomething( std::move(Objekt) ); std::cout << "Whatever\n"; }Hier kann der Compiler nicht automatisch das Objekt für den Parameter moven, weil er nicht weiß, ob es evtl in den Innereien von std::cout noch gebraucht wird; deshalb würde er es kopieren. Mit unserer Angabe von std::move sagen wir, dass wir das Objekt danach eben nicht mehr brauchen.
-
Siassei schrieb:
krümelkacker schrieb:
class A { static A createA() { A a; return a; }; }; //---------- A a = A::createA ();Wenn Du jetzt verhindern willst, dass das A-Objekt hier ggf kopiert wird, brauchst Du für A einen Move-Konstruktor.
Nur einen move-Constructor? Oder muss die Function nicht
static A&& createA() { A a; return a; };
heisen?Das Beispiel ist sehr einfach gehalten und ja, ich möchte das Kopieren verhindern.
z.B.
- ein Interface ist als Header-Only deklariert
- in einer DLL wird das Interface deklariert
- in einer weiteren DL (DynamicLibrary) wird nun die Instance für das Interface angefordert.
Ich kann mir nicht vorstellen, dass der Compiler über DL-Grenzen hinweg optimiert. Zudem möchte ich nun endlich verstehen, für was alles man das ganze Einsetzen kann und bin eben auf diesen Anwendungsfall gestossen.@Pi
Alles was C++0x betrifft bin ich absoluter Anfänger. Beim Rest würde ich mich nicht als solches Bezeichnen
Gruß, Thomas
Das '&&' ist nichts Anderes als ein Zeiger, aber einer, der sagt, dass das Objekt, auf das er zeigt, nicht mehr benötigt wird (es wird danach nur noch der Destruktor aufgerufen). Wenn du also eine rvalue reference auf ein Objekt auf dem Funktionsstack zurückgibst, zeigt der Pointer irgendwo in undefinierten Bereich, weil das Objekt schon zerstört wurde. Im Grunde verhält sich das wie bei normalen Referenzen auch.
Zum thema dlls und Optimierungen kann ich nichts genaues sagen, denke aber, dass es trotzdem funktioniert. Man kann ja der Funktion implizit einen Zeiger auf Speicher übergeben, auf den das Objekt dann konstruiert wird. Ich sehe nicht, warum das an dll Grenzen scheitern sollte.
-
Ich habe bisher zwei Dinge beobachtet. Leute, die sich ganz frisch mit dem Rvalue-Referenz-Thema (und dem ganzen Drumherum) auseinandersetzen gehen davon aus, dass...
- der Compiler irgendwie dafür zuständig ist, zu kontrollieren, wie Objekte von einem Ort zum anderen "bewegt" werden
- sie als Programmierer selbst kontrollieren müssen, wann ein Objekt von einem Ort zum anderen "bewegt" (statt kopiert) wird.
Es ist aber genau andersherum. Beispiel std::string. Es ist die Aufgabe desjenigen, der die Klasse schreibt, zu sagen, wie genau ein Objekt des Typs "bewegt" wird. Das passiert über die Move-Operationen (Move-Constructor, Move-Assignment-Operator). Wann ein Objekt bewegt statt kopiert werden kann, das weiß der Compiler selbst. Dafür gibt es Regeln. Diese Regeln sind einfach und sehr praktisch. Einfach deswegen, weil sie nicht fordern, dass ein Compiler komplizierte Analysen fahren muss, um zu sehen, wann wo was verwendet wird oder nicht. Praktisch deswegen, weil sie fast immer das richtige tun und im schlimmsten Fall eine unnötige Kopie erzeugen, die man dann aber per z.B. std::move() manuell umgehen kann.
class string { … public: string(string const&); // copy ctor string(string &&); // move ctor string& operator=(string const&); // copy assign string& operator=(string &&) ; // move assign ~sring(); // dtor … };Bei den Move-Operationen darf das Quell-Objekt verändert werden, weil eine frisch initialisierte Rvalue-Referenz nur auf ein Objekt verweisen kann, welches keinen mehr interessiert. Entweder, weil es ein temporäres Objekt war und diese Referenz der einzige Weg ist, das Objekt noch zu erreichen, oder, weil der Nutzer explicit std::move() oder std::forward<string>() verwendet hat und uns damit sagt "Das Ding interessiert mich nicht mehr". So kontrolliert der Klassenautor, was genau passieren soll. Typischerweise werden da nur ein paar Zeiger umgebogen und dabei das Quellobjekt "geleert". Es ist also eigentlich nur interessant für Objekte, die einen Teil des Zustands irgendwo anders speichern und sich Zeiger dahin merken. Für normale POD-structs ist das mit den move-Operationen völlig uninteressant.
Bei Funktionen wie dieser:
string flip(string x) { std::reverse(x.begin(),x.end()); return x; } int main() { string z = flip(flip("hello!")); // "z" ist ein Lvalue-Ausdruck // "std::move(z)" ist ein Rvalue-Ausdruck, // der sich auf _dasselbe_ Objekt bezieht // string w = z; // Lvalue --> Copy-ctor string w = std::move(z); // Rvalue --> Move-Ctor }muss der Nutzer nichts besonderes machen, um von der move-optimierten Klasse profitieren zu können. x bezieht sich auf ein Funktions-lokales Objekt. Die Regeln sagen, dass bei so etwas wie "return x;", wobei x sich auf ein solches Funktions-lokales Objekt bezieht, nur dann kopiert wird, wenn (a) der Compiler diesen Vorgang nicht wegoptimieren (copy elision) kann und (b) die Klasse keinen Move-Ctor hat. string hat hier aber einen Move-Ctor. Es wird also niemals im ganzen obigen Beispielprogramm unnötig kopiert.
Was macht std::move jetzt eigentlich? Die Antwort darauf ist: Nichts. Nichts, was irgendwie Zeit kosten würde. Es ist nur dazu da, um aus einem Lvalue ein Rvalue zu machen. Da wird nichts kopiert und nichts bewegt. Das, was std::move zurückgibt ist eine Referenz auf dasselbe Objekt, was man reingesteckt hat. Nur ist der std::move()-Ausdruck jetzt einer, der dazu führen kann, dass bei der Overload resolution eben ein move-ctor statt eines copy-ctors verwendet wird, respektive ein move-assign statt copy-assign -- sofern vorhanden. std::move() ist nur ein "Move Request". Wenn die Klasse diese Funktionen nicht anbietet, dann wird eben ganz normali kopiert.
Was sind jetzt Lvalues und Rvalues? Das ist leider mit das Komplizierteste, wenn man all die Details verstehen will. Es gibt keine schöne kurze Definition davon. Es gibt Faustregeln, Gedächtnisstützen, die aber nicht 100% korrekt sind. 100% korrekt sind Listen von Beispielen, die genau aufzählen, welche Ausdrücke welche "Wert-Kategorie" haben. Ich versuche es mal ganz allgemein: Ein Rvalue ist ein Ausdruck, der einen Wert liefert oder sich auf ein Objekt beziehet, für den/das sich sonst keiner mehr außer dem direkten Empfänger interessiert. Alles andere sind Lvalues. Ich komme aber wahrscheinlich nicht drum herum, Beispiele zu nennen:
- Nicht-String-Literale sind Rvalues (Beispiel: 3.1415)
- (4+5.0)*3 ist ein Rvalue
- i++ ist ein Rvalue (angenommen i ist ein int)
- Funktionsaufrufe, die keine Lvalue-Referenz zurückgeben -- einschließlich expliziter Konstruktor-Aufruf für temporäre Objekte a la string("foo") -- sind Rvalues
- Das Ergebnis einer implizierten Konvertierung ist ein Rvalue sofern der Ergebnis-Typ keine Lvalue-Referenz ist
- Namen, die sich direkt auf Objekte beziehen, sind Lvalues
- String-Literale sind Lvalues (beziehen sich auf langlebiges Array)
- ++i ist ein Lvalue (angenommen i ist ein int)
- Ein Zeiger-Dereferenzierungs-Ausdruck ist ein Lvalue (Beispiel: *ptr oder ptr[22])
- Funktionsaufrufe von Funktionen, die Lvalue-Referenzen zurückgeben, sind Lvalues (Beispiele: meinvektor[22], *mysharedptr)
Ich habe hier viele Fälle ausgelassen. Das müsste aber für's erste reichen. Und soooo wichtig ist das eigentlich auch nicht.
-
Hier gibt es einen brauchbaren Artikel zu dem Thema. Der beschreibt auch, was lvalues und rvalues überhaupt sind. Außerdem geht er auch auf die typischen Anfängerfehler bei den rvalue-Referenzen ein.
-
Ganz simpel zusammengefasst, machen Move-Konstruktoren und move-Assignment nur dann Sinn, wenn du in der Klasse eine oder mehrere Ressourcen nach RAII-Art hälst.
Bisher hieß Rvalue-Kopieren von solchen Klassen immer folgendes:Kopierendes Objekt legt neue Ressource an, die die Ressource des Originals kopiert.
Originalobjekt wird zerstört, mit ihm die Originalressource.Move-Operationen machen es wie folgt:
Kopierendes Objekt übernimmt die Ressource des Originals. Keine tiefen Kopien nötig, kein Anlegen der neuen Ressource nötig.
Originalobjekt wird (ohne Originalressource) zerstört.Stell dir eine sehr vereinfachte Version des std::vector vor. Er enhält einen Pointer auf die Daten (dynamisches Array) und ein size_t, das weiß, wieviele Daten es sind, d.h. wie groß das Array ist. Der Copy-Ctor kopiert den size_t, legt ein neues dynamisches Array an und kopiert den Inhalt des Original-Arrays ins neue Array. Das Original stirbt, zerstört alle Element und gibt das Array frei.
Der Move-Ctor kopiert den size_t (bei einfache Datentypen gibts nichts zu moven!), kopiert danach den Pointer und setzt den Pointer im Original auf 0. Nimmt also quasi dem Original seine Ressource (das dynamische Array) einfach weg. Das Original stirbt und nimmt nur seinen size_t mit ins Grab.
-
Tachyon, der von Dir verlinkte Artikel ist leider nicht mehr aktuell. Es gab seitdem zwei wichtige Änderungen bei den Rvalue-Referenz-Regeln. Heute verweist eine frisch initialisierte non-const Rvalue-Referenz immer nur auf ein Objekt, was verändert werden darf, ohne dass es jemanden stören würde. Anfang 2009 konnte man aber noch schreiben
int i = 0; int && rr = i;Das kompiliert jetzt aber nicht mehr. Man hat aus Sicherheitsgründen die Regeln abgeändert, weil es sonst zu versehentlichen Modifikationen von anderen Objekten hätte kommen können.
Wer gesprochenes amerikanisches Englisch versteht, kann sich ja mal Stephan T. Lavavej's Vortrag anhören.
-
Hallo,
ich habe mal in einer Klasse den Move-Copy-Ctor und den Move Assignment Operator implementiert. Falls etwas besser gemacht werden könnte, bitte sagen.#include <iostream> #include <vector> #include <string> class X { private: std::vector<std::string> * m_pResource; public: X():m_pResource(new std::vector<std::string>) {} ~X() { delete m_pResource; m_pResource = nullptr;} X(X const& other); //normal copy ctor X(X&& tmp); //move copy ctor X& operator= (X const& rhs); //normal assignment operator X& operator=(X&& tmp); //Move assignment operator }; X::X(X const& other) { std::cout << "Copy Ctor\n"; m_pResource = new std::vector<std::string>(*other.m_pResource); //teuer } X::X(X&& tmp) { std::cout << "Move Copy Ctor\n"; m_pResource = nullptr; std::swap(m_pResource,tmp.m_pResource); } X& X::operator=(X&& tmp) { std::cout << "Move Assignmnet Operator\n"; std::swap(m_pResource,tmp.m_pResource); return *this; } X& X::operator= (X const& rhs) { if (this != &rhs) { std::cout << "Assignment operator\n"; std::vector<std::string> * tmp = new std::vector<std::string>(*rhs.m_pResource); //teuer std::swap(m_pResource,tmp); } return *this; } X foo() { X x; return x; } int main() { X x; std::cout << "x = foo(); \n"; x = foo(); //Ohne Move Ctor und ohne Move Assignment operator wird zuerst der Copy-Ctor aufgerufen und anschließend der Assignment operator aufgerufen (ohne NRVO) //in beiden Fällen wird die Resource teuer kopiert //mit NRVO: nur der Assignment operator wird aufgerufen //mit Move Copy-Ctor und Move Assignmnet operator wird zuerst der MoveCopy-Ctor und anschließend der Move Assignment operator aufgerufen (ohne NRVO) //in beiden Fällen werden nur Pointer umgebogen //mit NRVO: nur der Move Assignment operator wird aufgerufen std::cout << "X y = x \n"; X y =x; //x ist kein rvlaue -> copy ctor std::cout << "X v; v = x;\n"; X v; v = x; //x ist kein rvlaue -> assignment operator std::cout << "X z;z = X();\n"; X z; z = X(); // X() ist ein Rvalue -> move assignment operator std::cout << "X t = std::move(z);\n"; X t = std::move(z);// z wird zum Rvalue -> move copy ctor (auf z sollte nicht mehr zugegriffen werden) std::cout << "X c;c = std::move(t);\n"; X c; c = std::move(t); //t wird zum Rvalue -> move assignment operator (auf t sollte nicht mehr zugegriffen werden) return 0; }
-
einwurf schrieb:
Falls etwas besser gemacht werden könnte, bitte sagen.
Der kopierende Zuweisungsoperator führt etwas aus, was du bereits implementiert hast (und auch noch falsch, weil du den Speicher nicht freigibst). Zweckmäßigerweise sollte er eher so aussehen (Copy+Move, statt wie bisher Copy+Swap):
X& X::operator= (X const& rhs) { std::cout << "Assignment operator\n"; return *this = X(rhs); }Außerdem prüfst du beim Kopieren nicht, ob evtl. das Argument bereits leer ist.
-
[quote="camper"]
einwurf schrieb:
X& X::operator= (X const& rhs) { return *this = X(rhs); }Was wird denn hier zurückgegeben? Also was ist der Rückgabewert einer zuweisung?
-
@camper, stimmt. Eine Speicherlücke.
Es geht aber noch einfacher (nur ein operator=), wenn ich nicht wieder etwas übersehen habe.class X { private: std::vector<std::string> * m_pResource; public: X():m_pResource(new std::vector<std::string>) {} ~X() { delete m_pResource; m_pResource = nullptr;} X(X const& other); //normal copy ctor X(X&& tmp); //move copy ctor X& operator= (X rhs); //Move assignment operator and normal assignment operator }; X::X(X const& other) { std::cout << "Copy Ctor\n"; m_pResource = new std::vector<std::string>(*other.m_pResource); //teuer } X::X(X&& tmp) { std::cout << "Move Copy Ctor\n"; m_pResource = nullptr; std::swap(m_pResource,tmp.m_pResource); } X& X::operator= (X rhs) { std::cout << "Move and normal assignment operator\n"; if(this != &rhs) { std::swap(m_pResource, rhs.m_pResource); } return *this; }
-
Wenn ich auch mal einen Einwurf machen dürfte: Die Dinger heißen copy constructor, copy assignment operator, move constructor und move assignment operator. Insbesondere "Move-Copy-Ctor" ergibt keinen Sinn.
-
@Bashar, danke. Ich dachte irrtümlich, weil der Copy-Ctor überladen wird heißt das nun ...
Das if(this != &rhs) kann man sich auch sparen, da "rhs" ja immer ein anderes Objekt als "this" ist.
Das swappen für den Fall x=x; sollte keine Probleme machen, oder?X& X::operator= (X rhs) { std::cout << "Move and normal assignment operator\n"; std::swap(m_pResource, rhs.m_pResource); return *this; }
-
einwurf schrieb:
@camper, stimmt. Eine Speicherlücke.
Es geht aber noch einfacher (nur ein operator=), wenn ich nicht wieder etwas übersehen habe....Das ist so möglich, aber im Movefall führst du so u.U. 2 Moves aus.
-
Fraaaggger schrieb:
camper schrieb:
X& X::operator= (X const& rhs) { return *this = X(rhs); }Was wird denn hier zurückgegeben? Also was ist der Rückgabewert einer zuweisung?
X(rhs) erzeugt ein temporäres Objekt, das dann für die Zuweisung genutzt wird. Weil es sich um ein rvalue handelt, wird die andere Zuweisungsüberladung aufgerufen, und somit das zurückgegeben, was die andere Funktion zurückgibt, also *this.
-
camper schrieb:
Fraaaggger schrieb:
camper schrieb:
X& X::operator= (X const& rhs) { return *this = X(rhs); }Was wird denn hier zurückgegeben? Also was ist der Rückgabewert einer zuweisung?
X(rhs) erzeugt ein temporäres Objekt, das dann für die Zuweisung genutzt wird. Weil es sich um ein rvalue handelt, wird die andere Zuweisungsüberladung aufgerufen, und somit das zurückgegeben, was die andere Funktion zurückgibt, also *this.
Ahh! Jetzt machts sinn, danke!
