Die C++ 11 std::move(x)
-Funktion bewegt überhaupt nichts. Es ist nur eine Besetzung auf R-Wert. Warum wurde das gemacht? Ist das nicht irreführend?
Es ist richtig, dass std::move(x)
nur eine Umwandlung in einen Wert ist - genauer gesagt in ein xvalue, im Gegensatz zu einem prvalue . Und es ist auch wahr, dass eine Besetzung mit dem Namen move
manchmal die Leute verwirrt. Diese Benennung soll jedoch nicht verwirren, sondern den Code besser lesbar machen.
Die Geschichte von move
geht zurück auf der ursprüngliche Vorschlag für einen Umzug im Jahr 2002 . In diesem Artikel wird zunächst die rWert-Referenz vorgestellt und anschließend gezeigt, wie Sie effizienter std::swap
Schreiben:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Man muss bedenken, dass zu diesem Zeitpunkt in der Geschichte das einzige, was "&&
" Möglicherweise bedeuten könnte, logisch und war. Niemand war mit rvalue-Referenzen vertraut und wusste auch nicht, wie sich das Umwandeln eines lvalue in einen rvalue auswirkt (ohne eine Kopie wie bei static_cast<T>(t)
zu erstellen). Leser dieses Codes würden also natürlich denken:
Ich weiß, wie
swap
funktionieren soll (nach temporär kopieren und dann die Werte austauschen), aber was ist der Zweck dieser hässlichen Casts ?!
Beachten Sie auch, dass swap
wirklich nur ein Ersatz für alle Arten von permutationsmodifizierenden Algorithmen ist. Diese Diskussion ist much, viel größer als swap
.
Dann führt der Vorschlag syntax sugar ein, das den static_cast<T&&>
Durch etwas besser lesbares ersetzt, das nicht das genaue what, sondern das Warum:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Das heißt move
ist nur Syntaxzucker für static_cast<T&&>
, und jetzt ist der Code ziemlich aussagekräftig, warum diese Casts vorhanden sind: um die Bewegungssemantik zu aktivieren!
Man muss verstehen, dass an dieser Stelle im Kontext der Geschichte nur wenige Menschen die enge Verbindung zwischen R-Werten und Bewegungssemantik wirklich verstanden haben (obwohl das Papier auch dies zu erklären versucht):
Die Verschiebungssemantik wird automatisch aktiviert, wenn rvalue-Argumente angegeben werden. Dies ist absolut sicher, da das Verschieben von Ressourcen aus einem Wert vom Rest des Programms nicht bemerkt werden kann ( niemand anderes hat einen Verweis auf den Wert, um einen Unterschied zu erkennen).
Wenn zu der Zeit stattdessen swap
wie folgt dargestellt wurde:
template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}
Dann hätten sich die Leute das angeschaut und gesagt:
Aber warum wirfst du auf Rvalue?
Der Hauptpunkt:
Unter Verwendung von move
hat noch nie jemand gefragt:
Aber warum ziehst du um?
Im Laufe der Jahre und der Verfeinerung des Vorschlags wurden die Begriffe lWert und rWert in die heutigen Kategorien Wertkategorien eingeteilt:
(Bild schamlos gestohlen von dirkgently )
Und wenn wir heute swap
genau sagen wollten was, dann sollte es statt warum so aussehen:
template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}
Und die Frage, die sich jeder stellen sollte, ist, ob der obige Code mehr oder weniger lesbar ist als:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Oder sogar das Original:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
In jedem Fall sollte der C++ - Programmierer wissen, dass unter der Haube von move
nichts weiter vor sich geht als eine Besetzung. Und der Anfänger C++ - Programmierer, zumindest mit move
, wird informiert, dass die Absicht besteht, move von der rechten Seite zu entfernen, im Gegensatz zu copy = von rhs, auch wenn sie nicht genau verstehen wie das ist vollbracht.
Wenn ein Programmierer diese Funktionalität unter einem anderen Namen wünscht, besitzt std::move
Darüber hinaus kein Monopol auf diese Funktionalität, und es ist keine nicht tragbare Sprachmagie in deren Implementierung involviert. Wenn man beispielsweise set_value_category_to_xvalue
Codieren und stattdessen verwenden möchte, ist dies trivial:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
In C++ 14 wird es noch prägnanter:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Wenn Sie also so geneigt sind, verzieren Sie Ihren static_cast<T&&>
, Wie Sie es für am besten halten, und möglicherweise entwickeln Sie eine neue Best Practice (C++ entwickelt sich ständig weiter).
Was macht move
in Bezug auf den generierten Objektcode?
Betrachten Sie dieses test
:
void
test(int& i, int& j)
{
i = j;
}
Kompiliert mit clang++ -std=c++14 test.cpp -O3 -S
Ergibt dies den folgenden Objektcode:
__Z4testRiS_: ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc
Wenn nun der Test geändert wird in:
void
test(int& i, int& j)
{
i = std::move(j);
}
Es gibt überhaupt keine Änderung im Objektcode. Man kann dieses Ergebnis verallgemeinern auf: Für trivial beweglich Objekte hat std::move
Keine Auswirkung.
Schauen wir uns nun dieses Beispiel an:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
Dies erzeugt:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc
Wenn Sie __ZN1XaSERKS_
Bis c++filt
Ausführen, wird Folgendes erzeugt: X::operator=(X const&)
. Kein Wunder hier. Wenn nun der Test geändert wird in:
void
test(X& i, X& j)
{
i = std::move(j);
}
Dann gibt es immer noch überhaupt keine Änderung im generierten Objektcode. std::move
Hat nichts getan, als j
in einen r-Wert umzuwandeln, und dieser r-Wert X
wird dann an den Kopierzuweisungsoperator von X
gebunden.
Fügen wir nun einen Verschiebungszuweisungsoperator zu X
hinzu:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Nun ändert sich der Objektcode does:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc
Das Ausführen von __ZN1XaSEOS_
Durch c++filt
Zeigt, dass X::operator=(X&&)
anstelle von X::operator=(X const&)
aufgerufen wird.
Und das ist alles, was es zu std::move
Gibt! Es verschwindet zur Laufzeit vollständig. Die einzige Auswirkung ist zur Kompilierungszeit, wenn might geändert wird, welche Überladung aufgerufen wird.
Lassen Sie mich hier ein Zitat aus dem C++ 11 FAQ von B. Stroustrup hinterlassen, das eine direkte Antwort auf die Frage von OP ist:
move (x) bedeutet "Sie können x als einen Wert behandeln". Vielleicht wäre es besser gewesen, wenn move () als rval () bezeichnet worden wäre, aber move () wird seit Jahren verwendet.
Übrigens hat mir das FAQ - Lesen lohnt sich.