Um zu verstehen, warum dies ein gutes Muster ist, sollten wir die Alternativen sowohl in C ++ 03 als auch in C ++ 11 untersuchen.
Wir haben die C ++ 03-Methode, um eine std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
In diesem Fall wird immer eine einzelne Kopie ausgeführt. Wenn Sie aus einer rohen C-Zeichenfolge std::string
erstellen, wird a erstellt und dann erneut kopiert: zwei Zuordnungen.
Es gibt die C ++ 03-Methode, einen Verweis auf a zu nehmen std::string
und ihn dann in einen lokalen zu tauschen std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
Das ist die C ++ 03-Version von "Verschiebungssemantik" und swap
kann oft so optimiert werden, dass sie sehr billig ist (ähnlich wie bei a move
). Es sollte auch im Kontext analysiert werden:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
und zwingt Sie, ein nicht temporäres zu bilden std::string
und es dann zu verwerfen. (Ein temporäres Objekt std::string
kann nicht an eine nicht konstante Referenz gebunden werden.) Es wird jedoch nur eine Zuordnung vorgenommen. Die C ++ 11-Version benötigt a &&
und erfordert, dass Sie es mit std::move
oder mit einem temporären Aufruf aufrufen. Dies erfordert, dass der Aufrufer explizit eine Kopie außerhalb des Aufrufs erstellt und diese Kopie in die Funktion oder den Konstruktor verschiebt.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Verwenden:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Als nächstes können wir die vollständige C ++ 11-Version erstellen, die sowohl das Kopieren als auch Folgendes unterstützt move
:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Wir können dann untersuchen, wie dies verwendet wird:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
Es ist ziemlich klar, dass diese 2-Überladungstechnik mindestens genauso effizient ist, wenn nicht sogar effizienter als die beiden oben genannten C ++ 03-Stile. Ich werde diese 2-Overload-Version als die "optimalste" Version bezeichnen.
Jetzt untersuchen wir die Version zum Kopieren:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
in jedem dieser Szenarien:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Wenn Sie diese Seite an Seite mit der "optimalsten" Version vergleichen, machen wir genau eine zusätzliche move
! Nicht ein einziges Mal machen wir ein Extra copy
.
Wenn wir also davon ausgehen, dass dies move
billig ist, bietet diese Version fast die gleiche Leistung wie die optimalste Version, jedoch zweimal weniger Code.
Und wenn Sie beispielsweise 2 bis 10 Argumente verwenden, ist die Reduzierung des Codes exponentiell - 2x weniger mit 1 Argument, 4x mit 2, 8x mit 3, 16x mit 4, 1024x mit 10 Argumenten.
Jetzt können wir dies durch perfekte Weiterleitung und SFINAE umgehen. So können Sie einen einzelnen Konstruktor oder eine einzelne Funktionsvorlage schreiben, die 10 Argumente akzeptiert, SFINAE ausführen, um sicherzustellen, dass die Argumente vom richtigen Typ sind, und sie dann in das Verzeichnis verschieben oder kopieren lokaler Staat nach Bedarf. Dies verhindert zwar das tausendfache Problem der Programmgröße, es kann jedoch immer noch eine ganze Reihe von Funktionen aus dieser Vorlage generiert werden. (Instanziierungen von Vorlagenfunktionen erzeugen Funktionen)
Viele generierte Funktionen bedeuten eine größere Größe des ausführbaren Codes, was die Leistung selbst verringern kann.
Für die Kosten von ein paar move
Sekunden erhalten wir kürzeren Code und nahezu die gleiche Leistung und sind oft einfacher zu verstehen.
Dies funktioniert nur, weil wir wissen, wenn die Funktion (in diesem Fall ein Konstruktor) aufgerufen wird, dass wir eine lokale Kopie dieses Arguments benötigen. Die Idee ist, dass wir, wenn wir wissen, dass wir eine Kopie erstellen werden, den Anrufer wissen lassen sollten, dass wir eine Kopie erstellen, indem wir sie in unsere Argumentliste aufnehmen. Sie können dann optimieren, dass sie uns eine Kopie geben (indem sie beispielsweise auf unser Argument eingehen).
Ein weiterer Vorteil der "Take by Value" -Technik besteht darin, dass Verschiebungskonstruktoren häufig keine Ausnahme sind. Dies bedeutet, dass die Funktionen, die nach Wert nehmen und aus ihrem Argument herausgehen, häufig keine Ausnahme sein können und alle throw
s aus ihrem Körper in den aufrufenden Bereich verschieben (Wer kann es manchmal durch direkte Konstruktion vermeiden oder die Elemente und move
in das Argument einbauen, um zu steuern, wo das Werfen stattfindet). Es lohnt sich oft, Methoden zu machen, die nicht geworfen werden.