wake-up-neo.net

Warum heißt "std :: move" "std :: move"?

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?

119
Howard Hinnant

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:

Taxonomy

(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.

165
Howard Hinnant

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.

19
podkova