move constructor
-
camper schrieb:
[...] IntVec &operator=(const IntVec &rhs) { IntVec tmp = rhs; swap( tmp ); return *this; } IntVec &operator=(IntVec &&rhs) noexcept { IntVec tmp = std::move(rhs); swap( tmp ); return *this; } [...]Da kann man auch
IntVec& operator = (IntVec other) { swap( other ); return *this; }schreiben. Man spart sich die doppelte Definition und es passiert das gleiche.
-
was macht
IntVec tmp = std::move(rhs);genau?
-
und warum wird hier rhs.arr und rhs.size auf 0 gesetzt?
IntVec (IntVec &&rhs) noexcept : arr( rhs.arr ), size(rhs.size) { rhs.arr = nullptr; rhs.size = 0; }
-
jeff1 schrieb:
und warum wird hier rhs.arr und rhs.size auf 0 gesetzt?
Weil irgendwann der Destruktor von rhs aufgerufen wird und man nicht mit einem dangling pointer hantieren möchte.
Size auf 0 setzen ist streng genommen nicht nötig.
-
Edit: gelöscht, hatte mich verlesen.
-
jeff1 schrieb:
was macht
IntVec tmp = std::move(rhs);genau?
move(rhs) gibt eine rvalue-Referenz auf rhs zurück, so dass tmp über den Move-Konstruktor erzeugt wird. Das kann bei üblicher Implementierung bedeuten, dass rhs danach leer ist. rhs ist ein lvalue, ohne move würde also der Kopierkonstruktor gerufen.
-
Marthog schrieb:
camper schrieb:
[...] IntVec &operator=(const IntVec &rhs) { IntVec tmp = rhs; swap( tmp ); return *this; } IntVec &operator=(IntVec &&rhs) noexcept { IntVec tmp = std::move(rhs); swap( tmp ); return *this; } [...]Da kann man auch
IntVec& operator = (IntVec other) { swap( other ); return *this; }schreiben. Man spart sich die doppelte Definition und es passiert das gleiche.
Das ist dann alledings kein move assignment operator mehr. Was in C++11 durchaus einen Unterschied macht. Speziell bei der Deklaration von impliziten move assignment Operatoren von Klassen, die ein
IntVecals Member haben.Tatsächlich ist das ein defect in C++11 und meine Anmerkung hier nur der Vollständigkeit halber. *
FW
* d.h. ich möchte diesen Thread nicht durch eine ellenlange Diskussion über Sprachfeinheiten ersetzen.
-
move(rhs) gibt eine rvalue-Referenz auf rhs zurück, so dass tmp über den Move-Konstruktor erzeugt wird. Das kann bei üblicher Implementierung bedeuten, dass rhs danach leer ist.
woher soll der user wissen ob der copy oder move constructor augerufen wird? im zweiten fall wird der operand rechts des zuweisungs operators ja NULL...
-
Bei RValues wird der Movekonstruktor aufgerufen, bei LValues der Copykonstruktor.
RValues sind z.B. std::move(lvalue), Konstruktoraufrufe.
Bei den temporären Objekten, die entstehen, wenn man eine Funktion void bar(foo f), so aufruft: bar(foo(1, 'a')), ist das egal, weil sie eh nicht lange leben.
Da ist das eine Optimierung.
Bspw. std::vector muss im Copyctor alle n Elemente von a nach b kopieren, beim move hingegen nur einen Pointer versetzen (wie in deinem Code).
Dass das viel schneller ist, sollte klar sein.
Hat man ein LValue und muss man es an einen anderen Ort haben und braucht man es danach nicht mehr, nutzt man std::move um das zum LValue zu machen (move ist im Prinzip nur ein cast!).
Das hat man z.B. in der Initialisierungsliste eines Konstruktors:
foo(costly_copyable c) c_(std::move(c)) {}
Da man c danach nicht mehr braucht, ist der State egal. move bietet sich hier an und ist schneller.
-
jeff1 schrieb:
woher soll der user wissen ob der copy oder move constructor augerufen wird?
Schau Dir mal die Referenz-Initialisierungs- und Überladungs-Regeln an, die dieses Verhalten erklären:
void senke(const int & lref); // #1 void senke( int && rref); // #2 int quelle() { return 42; } const int& dings() { static const int foo = 29; return foo; } int main() { int j = 23; senke(j); // ruft #1 auf, lref ist dann ein Synonym für j senke(dings()); // ruft #1 auf, lref ist dann ein Synonym für foo senke(j+0); // ruft #2 auf, rref bezieht sich auf einen temporären int senke(17); // ruft #2 auf, rref bezieht sich auf einen temporären int senke(quelle()); // ruft #2 auf, rref bezieht sich auf einen temporären int senke(std::move(j)); // ruft #2 auf, rref bezieht sich auf j return 0; }Lässt man die Rvalue-Referenz-Überladung (#2) weg, dann wird in allen Fällen hier #1 aufgerufen.
Die gleiche Überladung (so wie bei senke) kannst Du jetzt auch bei Konstruktoren und Zuweisungsoperatoren machen.
Der Witz an der ganzen Sache ist, dass eine frisch initialisierte Rvalue-Referenz sich immer nur auf etwas bezieht, was keinen mehr interessiert -- entweder, weil es ein temporäres Objekt war und kein anderer einen Weg hat, sich auf dieses noch zu beziehen, oder weil der Benutzer explizit sein Desinteresse am Zustand des Objekts (z.B. mit std::move) bekundet hat. Die Objekte, auf die sich Rvalue-Referenz-Parameter beziehen, darf man also verändern, ohne dass es jemanden stören würde. So gesehen, hätte man auch
std::moveinstd::i_dont_care_about_this_object_anymoreumbenennen können. Mehr macht dasstd::movenicht. Es ist nur dazu da, dass man als Nutzer sagen kann, dass einem ein eventuell veränderter Zustand des Objekts egal ist. Aber es wird typischerweise für Move-Semantics verwendet. Es sagt dem Compiler, dass es OK ist, statt des Kopierkonstruktors einen Movekonstruktor zu verwenden (falls es einen solchen gibt).