C++ hat schon immer schnelle Programme hervorgebracht. Leider gab es bis C++11 eine hartnäckige Warze, die viele C++-Programme verlangsamt hat: die Erzeugung temporärer Objekte. Manchmal können diese temporären Objekte vom Compiler wegoptimiert werden (z. B. durch die Rückgabewertoptimierung). Aber das ist nicht immer der Fall, und es kann zu teuren Objektkopien führen. Was meine ich damit?
Sagen wir, Sie haben folgenden Code:
#include <iostream>using namespace std;vector<int> doubleValues (const vector<int>& v){ vector<int> new_values; new_values.reserve(v.size()); for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr ) { new_values.push_back( 2 * *itr ); } return new_values;}int main(){ vector<int> v; for ( int i = 0; i < 100; i++ ) { v.push_back( i ); } v = doubleValues( v );}
Wenn Sie schon viel mit C++ gearbeitet haben, tut es Ihnen leid, dass das so schmerzhaft war. Wenn nicht – nun, lassen Sie uns durchgehen, warum dieser Code schrecklicher C++03-Code ist. (Der Rest dieses Tutorials wird sich damit befassen, warum es ein guter C++11-Code ist. Wenn doubleValues aufgerufen wird, wird ein Vektor, new_values, konstruiert und aufgefüllt. Das allein ist vielleicht nicht ideal für die Leistung, aber wenn wir unseren ursprünglichen Vektor unversehrt lassen wollen, brauchen wir eine zweite Kopie. Aber was passiert, wenn wir auf die Return-Anweisung stoßen?
Der gesamte Inhalt von new_values muss kopiert werden! Im Prinzip könnte es hier bis zu zwei Kopien geben: eine in ein temporäres Objekt, das zurückgegeben wird, und eine zweite, wenn der Vektorzuweisungsoperator in der Zeile v = doubleValues( v ); läuft. Die erste Kopie kann vom Compiler automatisch wegoptimiert werden, aber es lässt sich nicht vermeiden, dass die Zuweisung zu v alle Werte erneut kopieren muss, was eine neue Speicherzuweisung und eine weitere Iteration über den gesamten Vektor erfordert.
Dieses Beispiel mag etwas konstruiert sein – und natürlich kann man Wege finden, diese Art von Problem zu vermeiden – zum Beispiel, indem man den Vektor per Zeiger speichert und zurückgibt oder indem man einen Vektor zum Auffüllen übergibt. Keiner dieser Programmierstile ist jedoch besonders natürlich. Außerdem hat ein Ansatz, der die Rückgabe eines Zeigers erfordert, mindestens eine weitere Speicherzuweisung eingeführt, und eines der Designziele von C++ ist es, Speicherzuweisungen zu vermeiden.
Das Schlimmste an dieser ganzen Geschichte ist, dass das vondoubleValues zurückgegebene Objekt ein temporärer Wert ist, der nicht mehr benötigt wird. Wenn Sie die Zeile v = doubleValues( v ) haben, wird das Ergebnis von doubleValues( v ) einfach weggeworfen, sobald es kopiert wurde! Theoretisch sollte es möglich sein, die gesamte Kopie zu überspringen und einfach den Zeiger im temporären Vektor zu stehlen und ihn in v zu behalten. In C++03 lautete die Antwort, dass es keine Möglichkeit gab, festzustellen, ob ein Objekt temporär war oder nicht, man musste denselben Code im Zuweisungsoperator oder Kopierkonstruktor ausführen, egal woher der Wert kam, so dass kein Stehlen möglich war. In C++11 ist die Antwort – man kann!
Dafür sind rvalue-Referenzen und Move-Semantik da! Die Move-Semantik ermöglicht es Ihnen, unnötige Kopien zu vermeiden, wenn Sie mit temporären Objekten arbeiten, die kurz davor sind, sich aufzulösen, und deren Ressourcen sicher aus diesem temporären Objekt entnommen und von einem anderen verwendet werden können.
Die Move-Semantik stützt sich auf eine neue Funktion von C++11, die rvalue-Referenzen genannt wird und die Sie verstehen sollten, um wirklich zu verstehen, was hier passiert. Zunächst werden wir also darüber sprechen, was ein rWert ist, dann, was eine rWert-Referenz ist, und schließlich werden wir auf die Move-Semantik zurückkommen und darauf, wie sie mit rWert-Referenzen implementiert werden kann.
RWerte und lWerte – erbitterte Rivalen oder beste Freunde?
In C++ gibt es rWerte und lWerte. Ein lWert ist ein Ausdruck, dessen Adresse übernommen werden kann, ein Locator-Wert – im Wesentlichen stellt ein lWert ein (halb)permanentes Stück Speicher zur Verfügung. Sie können Zuweisungen an lWerte vornehmen. Ein Beispiel:
int a;a = 1; // here, a is an lvalue
Sie können auch lWerte haben, die keine Variablen sind:
int x;int& getRef () { return x;}getRef() = 4;
Hier gibt getRef einen Verweis auf eine globale Variable zurück, also einen Wert, der an einem permanenten Ort gespeichert ist. (Man könnte buchstäblich & getRef() schreiben, wenn man wollte, und man würde die Adresse von x erhalten.)
RWerte sind – nun, rWerte sind keine lWerte. Ein Ausdruck ist ein rWert, wenn er zu einem temporären Objekt führt. Zum Beispiel:
int x;int getVal (){ return x;}getVal();
Hier ist getVal() ein rWert – der zurückgegebene Wert ist kein Verweis auf x, sondern nur ein temporärer Wert. Etwas interessanter wird es, wenn wir reale Objekte anstelle von Zahlen verwenden:
string getName (){ return "Alex";}getName();
Hier gibt getName eine Zeichenkette zurück, die innerhalb der Funktion konstruiert wird. Sie können das Ergebnis von getName einer Variablen zuweisen:
string name = getName();
Aber Sie weisen von einem temporären Objekt zu, nicht von einem Wert, der einen festen Platz hat. getName() ist ein rvalue.
Erkennen von temporären Objekten mit rvalue-Referenzen
Das Wichtigste ist, dass rvalues auf temporäre Objekte verweisen – genau wie der von doubleValues zurückgegebene Wert. Wäre es nicht großartig, wenn wir ohne den Schatten eines Zweifels wissen könnten, dass ein von einem Ausdruck zurückgegebener Wert temporär ist, und irgendwie Code schreiben könnten, der überladen ist, um sich bei temporären Objekten anders zu verhalten? Ja, in der Tat, das wäre großartig. Und genau dafür sind rvalue-Referenzen gedacht. Eine rvalue-Referenz ist eine Referenz, die nur an ein temporäres Objekt gebunden wird. Was meine ich damit?
Vor C++11 konnte man, wenn man ein temporäres Objekt hatte, eine „reguläre“ oder „lvalue-Referenz“ verwenden, um es zu binden, aber nur, wenn sie konstant war:
const string& name = getName(); // okstring& name = getName(); // NOT ok
Die Intuition hier ist, dass man keine „veränderbare“ Referenz verwenden kann, weil man sonst ein Objekt ändern könnte, das bald verschwindet, und das wäre gefährlich. Beachten Sie übrigens, dass die Beibehaltung einer Constreference auf ein temporäres Objekt sicherstellt, dass das temporäre Objekt nicht sofort zerstört wird. Das ist eine nette Garantie von C++, aber es handelt sich immer noch um ein temporäres Objekt, das Sie nicht verändern wollen.
In C++11 gibt es jedoch eine neue Art von Referenz, eine „rvalue-Referenz“, mit der Sie eine veränderbare Referenz an einen r-Wert binden können, aber nicht an einen l-Wert. Mit anderen Worten: rvalue-Referenzen eignen sich perfekt, um festzustellen, ob ein Wert ein temporäres Objekt ist oder nicht. R-Wert-Referenzen verwenden die &&-Syntax anstelle von & und können const und non-const sein, genau wie l-Wert-Referenzen, obwohl Sie selten eine const r-Wert-Referenz sehen werden (wie wir sehen werden, sind veränderliche Referenzen der Sinn der Sache):
const string&& name = getName(); // okstring&& name = getName(); // also ok - praise be!
So weit ist das alles schön und gut, aber wie hilft es? Das Wichtigste an lvalue-Referenzen im Vergleich zu rvalue-Referenzen ist, was passiert, wenn man Funktionen schreibt, die lvalue- oder rvalue-Referenzen als Argumente nehmen. Nehmen wir an, wir haben zwei Funktionen:
printReference (const String& str){ cout << str;}printReference (String&& str){ cout << str;}
Jetzt wird das Verhalten interessant: Die Funktion printReference, die eine l-Wert-Referenz annimmt, akzeptiert jedes Argument, das ihr gegeben wird, unabhängig davon, ob es ein l-Wert oder ein r-Wert ist, und unabhängig davon, ob der l-Wert oder r-Wert veränderbar ist oder nicht. Wenn jedoch die zweite Überladung, printReference, eine rvalue-Referenz annimmt, erhält sie alle Werte außer veränderbaren Wert-Referenzen. Mit anderen Worten, wenn Sie schreiben:
string me( "alex" );printReference( me ); // calls the first printReference function, taking an lvalue referenceprintReference( getName() ); // calls the second printReference function, taking a mutable rvalue reference
Jetzt haben wir eine Möglichkeit, festzustellen, ob sich eine Referenzvariable auf ein temporäres oder ein permanentes Objekt bezieht. Die rWert-Referenzversion der Methode ist wie der geheime Hintereingang zum Club, in den man nur reinkommt, wenn man ein temporäres Objekt ist (langweiliger Club, denke ich). Nun, da wir unsere Methode haben, mit der wir feststellen können, ob ein Objekt temporär oder permanent ist, wie können wir sie verwenden?
Move-Konstruktor und Move-Zuweisungsoperator
Das häufigste Muster, das Sie bei der Arbeit mit rvalue-Referenzen sehen werden, ist die Erstellung eines Move-Konstruktors und eines Move-Zuweisungsoperators (der denselben Prinzipien folgt). Ein move-Konstruktor nimmt wie ein copy-Konstruktor eine Instanz eines Objekts als Argument und erzeugt eine neue Instanz auf der Grundlage des ursprünglichen Objekts. Der move-Konstruktor kann jedoch die Neuzuweisung von Speicher vermeiden, weil wir wissen, dass er ein temporäres Objekt erhalten hat. Anstatt die Felder des Objekts zu kopieren, werden wir sie verschieben.
Was bedeutet es, ein Feld des Objekts zu verschieben? Wenn das Feld ein primitiver Typ ist, wie int, kopieren wir es einfach. Interessanter wird es, wenn es sich bei dem Feld um einen Zeiger handelt: Anstatt neuen Speicher zu allozieren und zu initialisieren, können wir einfach den Zeiger stehlen und den Zeiger im temporären Objekt löschen! Wir wissen, dass das temporäre Objekt nicht mehr benötigt wird, also können wir den Zeiger aus ihm herausnehmen.
Stellen Sie sich eine einfache ArrayWrapper-Klasse vor, etwa so:
class ArrayWrapper{ public: ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; } private: int *_p_vals; int _size;};
Beachten Sie, dass der Kopierkonstruktor sowohl Speicher zuweisen als auch jeden Wert aus dem Array kopieren muss, einen nach dem anderen! Das ist eine Menge Arbeit für eine Kopie. Fügen wir einen move-Konstruktor hinzu und erreichen wir eine enorme Effizienz.
class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _size( 64 ) {} ArrayWrapper (int n) : _p_vals( new int ) , _size( n ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _size( other._size ) { other._p_vals = NULL; other._size = 0; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _size( other._size ) { for ( int i = 0; i < _size; ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; int _size;};
Wow, der move-Konstruktor ist tatsächlich einfacher als der copy-Konstruktor! Das ist ein ziemliches Kunststück. Die wichtigsten Dinge sind:
- Der Parameter ist eine nicht-konstante rWert-Referenz
- andere._p_vals wird auf NULL gesetzt
Die zweite Beobachtung erklärt die erste – wir könnten andere._p_vals nicht aufNULL setzen, wenn wir eine const rWert-Referenz genommen hätten. Aber warum müssen wir other._p_vals = NULL setzen? Der Grund ist der Destruktor – wenn das temporäre Objekt aus dem Geltungsbereich herausgeht, wird wie bei allen anderen C++-Objekten auch sein Destruktor ausgeführt, der dann _p_vals freigibt. Die gleichen _p_vals, die wir gerade kopiert haben! Wenn wir other._p_vals nicht auf NULL setzen, wäre die Verschiebung nicht wirklich eine Verschiebung, sondern nur eine Kopie, die später zu einem Absturz führt, wenn wir anfangen, den freigegebenen Speicher zu verwenden. Das ist der Sinn eines move-Konstruktors: eine Kopie zu vermeiden, indem man das ursprüngliche, temporäre Objekt ändert!
Auch hier funktionieren die Überladungsregeln so, dass der move-Konstruktor nur für ein temporäres Objekt aufgerufen wird – und nur für ein temporäres Objekt, das geändert werden kann. Das bedeutet, dass eine Funktion, die ein Konst-Objekt zurückgibt, dazu führt, dass der Copy-Konstruktor anstelle des Move-Konstruktors ausgeführt wird – schreiben Sie also keinen Code wie diesen:
const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!
Es gibt noch eine weitere Situation, die wir noch nicht besprochen haben, wie man mit einem Move-Konstruktor umgeht – wenn wir ein Feld haben, das ein Objekt ist. Stellen Sie sich zum Beispiel vor, dass wir statt eines Größenfeldes ein Metadatenfeld haben, das so aussieht:
class MetaData{public: MetaData (int size, const std::string& name) : _name( name ) , _size( size ) {} // copy constructor MetaData (const MetaData& other) : _name( other._name ) , _size( other._size ) {} // move constructor MetaData (MetaData&& other) : _name( other._name ) , _size( other._size ) {} std::string getName () const { return _name; } int getSize () const { return _size; } private: std::string _name; int _size;};
Jetzt kann unser Array einen Namen und eine Größe haben, also müssen wir die Definition von ArrayWrapper vielleicht so ändern:
class ArrayWrapper{public: // default constructor produces a moderately sized array ArrayWrapper () : _p_vals( new int ) , _metadata( 64, "ArrayWrapper" ) {} ArrayWrapper (int n) : _p_vals( new int ) , _metadata( n, "ArrayWrapper" ) {} // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { other._p_vals = NULL; } // copy constructor ArrayWrapper (const ArrayWrapper& other) : _p_vals( new int ) , _metadata( other._metadata ) { for ( int i = 0; i < _metadata.getSize(); ++i ) { _p_vals = other._p_vals; } } ~ArrayWrapper () { delete _p_vals; }private: int *_p_vals; MetaData _metadata;};
Läuft das so? Es scheint doch ganz natürlich zu sein, den MetaDatamove-Konstruktor aus dem Move-Konstruktor für ArrayWrapper heraus aufzurufen, oder nicht? Das Problem ist, dass dies einfach nicht funktioniert. Der Grund ist einfach: der Wert von other im move-Konstruktor ist eine rvalue-Referenz. Aber eine rvalue-Referenz ist nicht wirklich ein rvalue. Es ist ein lvalue, und so wird der copy-Konstruktor aufgerufen, nicht derove -Konstruktor. Das ist seltsam. Ich weiß – es ist verwirrend. Hier ist die Art, wie man darüber nachdenkt. Ein rvalue ist ein Ausdruck, der ein Objekt erzeugt, das sich gerade in Luft auflöst. Es befindet sich in den letzten Zügen seines Lebens – oder kurz davor, seinen Lebenszweck zu erfüllen. Plötzlich übergeben wir das temporäre Objekt an einen Move-Konstruktor, und es beginnt ein neues Leben in dem neuen Bereich. In dem Kontext, in dem der rvalue-Ausdruck ausgewertet wurde, ist das temporäre Objekt wirklich vorbei und erledigt. In unserem Konstruktor hat das Objekt jedoch einen Namen; es lebt für die gesamte Dauer unserer Funktion weiter, d. h. wir verwenden die Variable other möglicherweise mehr als einmal in der Funktion, und das temporäre Objekt hat einen definierten Ort, der wirklich für die gesamte Funktion bestehen bleibt. Es handelt sich um einen L-Wert im eigentlichen Sinne des Begriffs Locator-Wert, wir können das Objekt an einer bestimmten Adresse lokalisieren, die für die gesamte Dauer des Funktionsaufrufs stabil ist. Möglicherweise wollen wir ihn später in der Funktion verwenden. Wenn ein move-Konstruktor immer dann aufgerufen würde, wenn wir ein Objekt in einer rValue-Referenz halten, könnten wir versehentlich ein verschobenes Objekt verwenden!
// move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( other._metadata ) { // if _metadata( other._metadata ) calls the move constructor, using // other._metadata here would be extremely dangerous! other._p_vals = NULL; }
Eine letzte Möglichkeit: Sowohl lvalue- als auch rvalue-Referenzen sind lvalue-Ausdrücke. Der Unterschied besteht darin, dass eine lvalue-Referenz eine Konstante sein muss, um einen Verweis auf einenrvalue zu enthalten, während eine rvalue-Referenz immer einen Verweis auf einen rvalue enthalten kann.Es ist wie der Unterschied zwischen einem Zeiger und dem, worauf gezeigt wird. Das Ding, auf das gezeigt wird, stammt von einem rWert, aber wenn wir die rWert-Referenz selbst verwenden, ergibt das einen lWert.
std::move
Was ist also der Trick, um diesen Fall zu behandeln? Wir müssen std::move verwenden, von<utility>–std::move ist eine Möglichkeit zu sagen, „ok, ehrlich gesagt, ich weiß, dass ich einen l-Wert habe, aber ich möchte, dass er ein r-Wert ist.“ std::move bewegt an und für sich nichts; es verwandelt nur einen l-Wert in einen r-Wert, so dass man den move-Konstruktor aufrufen kann. Unser Code sollte wie folgt aussehen:
#include <utility> // for std::move // move constructor ArrayWrapper (ArrayWrapper&& other) : _p_vals( other._p_vals ) , _metadata( std::move( other._metadata ) ) { other._p_vals = NULL; }
Und natürlich sollten wir zu MetaData zurückgehen und seinen eigenen move-Konstruktor so anpassen, dass er std::move für die Zeichenkette verwendet, die er enthält:
MetaData (MetaData&& other) : _name( std::move( other._name ) ) // oh, blissful efficiency : _size( other._size ) {}
Move-Zuweisungsoperator
Genauso wie wir einen move-Konstruktor haben, sollten wir auch einen move-Zuweisungsoperator haben. Man kann ihn leicht schreiben, indem man die gleichen Techniken wie bei der Erstellung eines Move-Konstruktors anwendet.
Move-Konstruktoren und implizit erzeugte Konstruktoren
Wie Sie wissen, wird in C++, wenn Sie einen Konstruktor deklarieren, der Compiler nicht mehr den Standardkonstruktor für Sie erzeugen. Dasselbe gilt auch hier: Wenn Sie einer Klasse einen Verschiebekonstruktor hinzufügen, müssen Sie Ihren eigenen Standardkonstruktor deklarieren und definieren. Andererseits verhindert die Deklaration eines move-Konstruktors nicht, dass der Compiler einen implizit erzeugten copyconstructor bereitstellt, und die Deklaration eines move-Zuweisungsoperators verhindert nicht die Erzeugung eines Standard-Zuweisungsoperators.
Wie funktioniert std::move
Sie fragen sich vielleicht, wie man eine Funktion wie std::move schreibt? Wie kommt man zu dieser magischen Eigenschaft, einen lWert in einenrWertbezug zu verwandeln? Die Antwort ist, wie Sie sich vielleicht denken können, typecasting. Die eigentliche Deklaration von std::move ist etwas komplizierter, aber im Grunde ist es nur ein static_cast in eine rWert-Referenz. Das bedeutet eigentlich, dass Sie move nicht wirklich verwenden müssen – aber Sie sollten es tun, da es viel klarer ist, was Sie meinen. Die Tatsache, dass ein Cast erforderlich ist, ist übrigens eine sehr gute Sache! Es bedeutet, dass Sie nicht versehentlich einen l-Wert in einen r-Wert umwandeln können, was gefährlich wäre, da dies eine versehentliche Verschiebung ermöglichen könnte. Sie müssen explizit std::move (oder einen cast) verwenden, um einen lWert in eine rWert-Referenz umzuwandeln, und eine rWert-Referenz wird niemals von sich aus an einen lWert gebunden.
Rückgabe einer expliziten rWert-Referenz aus einer Funktion
Gibt es Fälle, in denen Sie eine Funktion schreiben sollten, die eine rWert-Referenz zurückgibt? Was bedeutet es überhaupt, eine rvalue-Referenz zurückzugeben? Sind Funktionen, die Objekte als Wert zurückgeben, nicht bereits rWerte?
Lassen Sie uns zuerst die zweite Frage beantworten: Die Rückgabe einer expliziten rWert-Referenz ist etwas anderes als die Rückgabe eines Objekts als Wert. Nehmen wir das folgende einfache Beispiel:
int x;int getInt (){ return x;}int && getRvalueInt (){ // notice that it's fine to move a primitive type--remember, std::move is just a cast return std::move( x );}
Im ersten Fall wird trotz der Tatsache, dass getInt() ein rWert ist, eindeutig eine Kopie der Variablen x erstellt. Wir können das sogar sehen, indem wir eine kleine Hilfsfunktion schreiben:
void printAddress (const int& v) // const ref to allow binding to rvalues{ cout << reinterpret_cast<const void*>( & v ) << endl;}printAddress( getInt() ); printAddress( x );
Wenn Sie dieses Programm ausführen, werden Sie sehen, dass zwei verschiedene Werte gedruckt werden.
Auf der anderen Seite,
printAddress( getRvalueInt() ); printAddress( x );
wird derselbe Wert gedruckt, weil wir hier explizit einen r-Wert zurückgeben.
Eine rvalue-Referenz zurückzugeben ist also etwas anderes als eine rvalue-Referenz nicht zurückzugeben, aber dieser Unterschied macht sich am deutlichsten bemerkbar, wenn Sie ein bereits existierendes Objekt haben, das Sie zurückgeben, anstatt ein temporäres Objekt, das in der Funktion erstellt wird (wo der Compiler wahrscheinlich die Kopie für Sie eliminiert).
Nun zu der Frage, ob Sie das tun wollen. Die Antwort lautet: wahrscheinlich nicht. In den meisten Fällen wird es dadurch nur wahrscheinlicher, dass Sie eine Dangling-Referenz erhalten (ein Fall, in dem die Referenz existiert, aber das temporäre Objekt, auf das sie sich bezieht, zerstört wurde). Das Problem ähnelt der Gefahr, die bei der Rückgabe einer lvalue-Referenz besteht – das Objekt, auf das verwiesen wird, existiert möglicherweise nicht mehr. Die Rückgabe einer rvalue-Referenz würde in erster Linie in sehr seltenen Fällen Sinn machen, in denen Sie eine Member-Funktion haben und das Ergebnis des Aufrufs vonstd::move auf einem Feld der Klasse aus dieser Funktion zurückgeben müssen – und wie oft werden Sie das tun?
Move-Semantik und die Standardbibliothek
Zurück zu unserem ursprünglichen Beispiel – wir haben einen Vektor verwendet, und wir haben keine Kontrolle über die Vektorklasse und darüber, ob sie einen Move-Konstruktor oder einen Move-Zuweisungsoperator hat oder nicht. Glücklicherweise ist das Standardisierungskomitee weise, und die Move-Semantik wurde der Standardbibliothek hinzugefügt. Das bedeutet, dass Sie nun effizient Vektoren, Maps, Strings und andere Standardbibliotheksobjekte zurückgeben können und dabei die Vorteile der Move-Semantik voll ausschöpfen können.
Bewegliche Objekte in STL-Containern
Die Standardbibliothek geht sogar noch einen Schritt weiter. Wenn Sie die Move-Semantik in Ihren eigenen Objekten aktivieren, indem Sie Move-Zuweisungsoperatoren und Move-Konstruktoren erstellen, verwendet die STL automatisch std::move, wenn Sie diese Objekte in einem Container speichern, und nutzt so automatisch die Vorteile von Move-aktivierten Klassen, um ineffiziente Kopien zu vermeiden.