C++0x: RValue reference und move constructor
Verfasst: Do Jun 09, 2011 11:19 pm
Hallo. Ich wollt nur mal kurz erwähnen, was denn das neue C++ tolles hervorgebracht hat.
Das Problem:
Betrachtet folgende Situation:
Ich möchte ein Objekt aus einem anderen Objekt initialisieren, z.B. so:
Möchte ich das machen, benötige ich einen sogenannten Kopierkunstruktor, also in struct A so etwas wie:
Soweit sogut. Dieser Fall, dass ich tatsächlich unterschiedliche Objekte habe, die danach auch beide benutzt werden tritt in meiner Erfahrung eher selten ein. Oft initialisiere ich ein Objekt über ein anderes, temporäres Objekt
Ich könnte auch eine "Fabrikfunktion" bereitstellen, die das gleiche tut:
Der Haken an der Sache: A wird hier jedes mal erstellt, kopiert und anschließend wieder gelöscht! Bei einer trivialen Klasse mit ein paar wenigen Membern macht das keinen Unterschied, aber wie siehts mit Klassen aus die größere Daten beinhalten?
Betrachtet folgenden Code:
Die Ausgabe des Programms ist folgende:
Für die Zuweisungen, initialisierungen aus temporären Objekten und initialisierung aus "Fabriksfunktionen" wurde jedes mal ein Objekt erstellt, das andere Objekt damit initialisiert und anschließend das temporäre Objekt wieder gelöscht.
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:
Der Unterschied wird erst bemerkbar, wenn man Versucht ein temporäres Objekt daran zu binden:
a_rr bleibt nun nämlich im Gesamten Gültigkeitsbereich am Leben. Wofür kann ich das brauchen fragt ihr euch? Nun, zugegeben in diesem Beispiel noch nicht. Allerdings können jetzt auch Funktionen RValue-Referenzen als Parameter überreicht bekommen und den (möglicherweise temporären) Wert verändern.
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:
Wie ihr seht, sind hier die die teuersten Operationen verschwunden. Kein allokieren, kein Kopieren, ihr "klaut" euch einfach die Daten der RValue Referenz. "Zur Sicherheit" werden die Daten der RValue-Referenz auf ungültige Werte gesetzt (also z.B. NULL).
Jetzt probieren wir diese veränderte Klasse mal aus:
Die Ausgabe sieht nun folgendermaßen aus:
Wie ihr seht, werden die temporären Objekte nun zwar immer noch erstellt und anschließend gelöscht, allerdings werden die enthaltenen Daten und Ressourcen übertragen anstatt einfach kopiert. Das spart Laufzeit.
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
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
