Es ist richtig, dass dies std::move(x)
nur eine Umwandlung in rvalue ist - genauer gesagt in einen xvalue im Gegensatz zu einem prvalue . Und es ist auch wahr, dass eine Besetzung move
manchmal die Leute verwirrt. Die Absicht dieser Benennung ist jedoch nicht zu verwirren, sondern Ihren Code lesbarer zu machen.
Die Geschichte von move
geht auf den ursprünglichen Umzugsvorschlag von 2002 zurück . In diesem Artikel wird zunächst die r-Wert-Referenz vorgestellt und anschließend gezeigt, wie Sie effizienter schreiben können std::swap
:
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 sich daran erinnern, dass zu diesem Zeitpunkt in der Geschichte das einzige, was " &&
" möglicherweise bedeuten könnte, logisch und . Niemand war mit den r-Wert-Referenzen oder den Auswirkungen des Umwandelns eines l-Werts in einen r-Wert vertraut (ohne eine Kopie zu erstellen, wie dies der Fall static_cast<T>(t)
wäre). Die Leser dieses Codes würden also natürlich denken:
Ich weiß, wie swap
es funktionieren soll (in temporäre kopieren und dann die Werte austauschen), aber was ist der Zweck dieser hässlichen Casts?!
Beachten Sie auch, dass dies swap
wirklich nur ein Ersatz für alle Arten von permutationsmodifizierenden Algorithmen ist. Diese Diskussion ist viel , viel größer als swap
.
Dann führt der Vorschlag Syntaxzucker ein, der den ersetztstatic_cast<T&&>
durch etwas Lesbareres ersetzt, das nicht das genaue Was , sondern das Warum vermittelt :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Dh move
ist nur Syntaxzucker für static_cast<T&&>
, und jetzt ist der Code ziemlich suggestiv, warum diese Casts da sind: um die Bewegungssemantik zu ermöglichen!
Man muss verstehen, dass im Kontext der Geschichte zu diesem Zeitpunkt nur wenige Menschen den engen Zusammenhang zwischen Werten und Bewegungssemantik wirklich verstanden haben (obwohl das Papier auch versucht, dies zu erklären):
Die Bewegungssemantik wird automatisch ins Spiel gebracht, 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 r-Wert, um einen Unterschied zu erkennen ).
Wenn zu der Zeit swap
stattdessen so präsentiert 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 gießt du auf rvalue?
Der Hauptpunkt:
So wie es war, move
fragte niemand jemals:
Aber warum ziehst du um?
Im Laufe der Jahre und als der Vorschlag verfeinert wurde, wurden die Begriffe lWert und rWert in die Wertkategorien verfeinert, die wir heute haben:
(Bild schamlos von dirkgent gestohlen )
Wenn wir heute swap
genau sagen wollen, was es tut, anstatt warum , sollte es eher 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 Gesellen-C ++ - Programmierer wissen, dass unter der Haube von move
nichts weiter vor sich geht als eine Besetzung. Und der C ++ - Programmierer für Anfänger, zumindest mit move
, wird darüber informiert, dass die Absicht darin besteht, sich von der rhs zu entfernen, anstatt von der rhs zu kopieren , auch wenn er nicht genau versteht, wie dies erreicht wird.
Wenn ein Programmierer diese Funktionalität unter einem anderen Namen wünscht, std::move
besitzt er kein Monopol auf diese Funktionalität und es ist keine nicht portable Sprachmagie an ihrer Implementierung beteiligt. Wenn man beispielsweise codieren set_value_category_to_xvalue
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, dekorieren Sie Ihre, static_cast<T&&>
wie Sie es für am besten halten, und vielleicht entwickeln Sie am Ende eine neue Best Practice (C ++ entwickelt sich ständig weiter).
Was macht move
also der generierte Objektcode?
Bedenken Sie Folgendes test
:
void
test(int& i, int& j)
{
i = j;
}
Kompiliert mit clang++ -std=c++14 test.cpp -O3 -S
erzeugt dies diesen 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 zu:
void
test(int& i, int& j)
{
i = std::move(j);
}
Es gibt absolut keine Veränderung überhaupt in dem Objektcode. Man kann dieses Ergebnis verallgemeinern auf: Für trivial bewegliche Objekte,std::move
hat dies keine Auswirkungen.
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 es __ZN1XaSERKS_
durchlaufen c++filt
, entsteht : X::operator=(X const&)
. Kein Wunder hier. Wenn nun der Test geändert wird zu:
void
test(X& i, X& j)
{
i = std::move(j);
}
Dann ändert sich der generierte Objektcode immer noch nicht . std::move
hat nichts anderes getan, j
als einen r-Wert umzuwandeln, und dann X
bindet dieser r-Wert an den Kopierzuweisungsoperator von X
.
Fügen Sie nun einen Verschiebungszuweisungsoperator hinzu zu X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Jetzt ändert sich der Objektcode :
__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
Laufen __ZN1XaSEOS_
durch c++filt
zeigt , dass X::operator=(X&&)
statt aufgerufen wirdX::operator=(X const&)
.
Und das ist alles was es zu std::move
tun gibt ! Es verschwindet zur Laufzeit vollständig. Die einzige Auswirkung ist die Kompilierungszeit, in der möglicherweise geändert wird, welche Überladung aufgerufen wird.
std::move
bewegt sich das Drei-Argument tatsächlich.