Das Problem:
Betrachtet folgende Situation:
Ich möchte ein Objekt aus einem anderen Objekt initialisieren, z.B. so:
Code: Alles auswählen
A a1(1);
A a2 = a1;
Code: Alles auswählen
A(const A& other) : x(other.x), y(other.y) {}
Code: Alles auswählen
a1 = A(2);
Code: Alles auswählen
A makeA(int x)
{ return A(2); }
a1 = makeA(2);
Betrachtet folgenden Code:
Code: Alles auswählen
struct A
{
static const std::size_t ALOT = 1000;
// default constructor
A()
{
std::cout<<(this)<<": Hi!\n";
hugedata = new int[ALOT];
}
// copy constructor
A(const A& other)
{
std::cout<<this<<" was copy constructed.\n";
hugedata = new int[ALOT];
std::copy(other.hugedata, other.hugedata+ALOT, hugedata);
}
// assignment operator
A& operator = (const A& other)
{
std::cout<<this<<" was assigned.\n";
if (this == &other) return *this;
hugedata = new int[ALOT];
std::copy(other.hugedata, other.hugedata+ALOT, hugedata);
return *this;
}
void whatup()
{
std::cout<<"Whatup? "<<*hugedata<<'\n';
}
~A()
{
if (hugedata)
{
delete[] hugedata;
hugedata = NULL;
}
std::cout<<this<<": Bye!\n";
}
private:
int *hugedata;
};
A makeA()
{
return A();
}
int main()
{
std::cout<<"Default-constructing: \n";
A a1;
/* A& a_lr = A(); // <-- Leider nein!!
a_lr.whatup(); */
std::cout<<"Copy-Constructing:\n";
A a2 = a1;
std::cout<<"Assignment from temporary object:\n";
a1 = A();
std::cout<<"Assignment from factory:\n";
a2 = makeA();
std::cout<<"Free to go.\n";
}
Code: Alles auswählen
Default-constructing:
0044F75C: Hi!
Copy-Constructing:
0044F750 was copy constructed.
Assignment from temporary object:
0044F678: Hi!
0044F75C was assigned.
0044F678: Bye!
Assignment from factory:
0044F684: Hi!
0044F750 was assigned.
0044F684: Bye!
Free to go.
0044F750: Bye!
0044F75C: Bye!
Wie ihr in diesem Beispiel seht, musste dazu jedes mal ein neues Array allokiert, die Daten kopiert und das Array wieder gelöscht werden!
Das braucht Zeit und Ressourcen, ist sehr unschön und sollte vermieden werden.
Wollte man das bisher vermeiden, musste man bisher mit Zeigern arbeiten. Das ist umständlich, denn auch wenn man sich mit "smarten" Zeigern das Leben erleichtert, das Objekt allokieren muss man sowieso. Außerdem arbeite ich persönlich viel lieber mit Referenzen, da ich dabei die Garantie habe dass das Objekt existiert, und nicht ein "Wenn dieses new funktioniert hat und kein anderer das Objekt gelöscht hat".
Die Lösung
Das neue C++ schafft abhilfe mit der sogenannten RValue-Referenz.
Eine RValue-Referenz tut erstmal das gleiche wie eine normale (LValue-Referenz), man kann Objekte daran binden und sie dann wie das Objekt selbst verwenden:
Code: Alles auswählen
A a;
A& lr = a; // Binde a an gewöhnliche LValue-Referenz
A&& rr = a; // Binde a an RValue-Referenz
lr.machwas();
rr.machwasanderes();
Code: Alles auswählen
A& a_lr = A(); // Fehler! "cannot convert from 'A' to 'A &' A non-const reference may only be bound to an lvalue"
A&& a_rr = A(); // Geht klar.
Besonders interessant wird das jetzt für Konstruktoren und Zuweisungsoperatoren! Wir können jetzt nämlich einen ganzen RValue, mitsamt seiner ganzen Daten und Ressourcen, den wir von einem temporären Objekt erhalten haben an uns reißen!
Dazu definieren wir den "move-constructor", und den "move-assignment"-Operator:
Code: Alles auswählen
// move constructor
A(A&& other)
{
std::cout<<this<<" was move constructed.\n";
hugedata = other.hugedata;
other.hugedata = NULL;
}
// move assignment
A& operator = (A&& other)
{
std::cout<<this<<" was move-assigned.\n";
if (this == &other) return *this;
hugedata = other.hugedata;
other.hugedata = NULL;
return *this;
}
Jetzt probieren wir diese veränderte Klasse mal aus:
Code: Alles auswählen
std::cout<<"Default-constructing: \n";
A a1;
std::cout<<"Move-Constructing:\n";
A a2(std::move(a1)); // a1 hat nun seine ressourcen aufgegeben!
std::cout<<"Assignment from temporary object:\n";
a1 = A();
std::cout<<"Assignment from factory:\n";
a2 = makeA();
std::cout<<"Free to go.\n";
Code: Alles auswählen
===== Testing B: =====
Default-constructing:
0048F8A0: Hi!
Move-Constructing:
0048F894 was move constructed.
Assignment from temporary object:
0048F7BC: Hi!
0048F8A0 was move-assigned.
0048F7BC: Bye!
Assignment from factory:
0048F7C8: Hi!
0048F894 was move-assigned.
0048F7C8: Bye!
Free to go.
0048F894: Bye!
0048F8A0: Bye!
Vielleicht fragt ihr euch, was denn nun das std::move() darstellen soll. Das tut nämlich folgendes:
Normalerweise wird bei der Überladung die LValue-Referenz bevorzugt. Das ist eine Sicherheitsmaßnahme, damit nicht unabsichtlich das Objekt zerstört wird, wenn man eigentlich kopieren wollte (und um bereits vorhandenen Coden nicht zu zerstören). Deshalb wird hier explizit dargestellt dass man die move-Semantik verwenden möchte, und das funktioniert eben über std::move.
Und wozu brauch ich das jetzt in Echt?
1) Wie gesagt, wenn ihr verhindern wollt das große Daten und Ressourcen kopiert werden müssen
2) Implementierung von "Quellen und Senken": ihr könnt ein einziges Objekt nun tatsächlich weitergeben (anstatt zu kopieren), so können Beispielsweise "Fabrikfunktionen" implementiert werden, die ein Objekt auf die richtige Weise (was auch immer das sein mag) erstellt, es initialisiert und dann weitergibt - und das ohne Overhead!
3) Implementierung von "Perfect Forwarding", also das erstellen von generischen (im Sinne von Templates) Wrappern
Wo kompiliert das Teil jetzt überhaupt?
Visual Studio ab 2010,
GCC ab 4.3 (mit -std=c++0x)
Und vielleicht noch ein paar, die mir aber leider nicht bekannt sind.
Den Code aus diesem Post könnt ihr in einem kleinen Testprojekt herunterladen: Es sollte sowohl mit Visual Studio als auch mit GCC kompilieren.
Also Leute, lesen, denken, ausprobieren und einsetzen!!
Gehet hin und implementieret es!
Natürlich würde ich mich sehr über Rückmeldungen freuen und ich werde versuchen Fragen so gut wie möglich zu beantworten. (Erwartet nicht zu viel, das Feature ist auch für mich neu!)
Ganz besonders interessant wären unerwartete Probleme oder Fallen die eventuell auftauchen, oder die ihr euch einfallen lasst.
Hoffe auf eine angeregte Diskussion,
euer fat-c++0x-lobyte
