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

128

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?

Howard Hinnant
quelle
Um die Sache noch schlimmer zu machen, std::movebewegt sich das Drei-Argument tatsächlich.
Cubbi
Und vergessen Sie nicht die C ++ std::char_traits::move
98/03/11
30
Mein anderer Favorit ist std::remove(), dass die Elemente nicht entfernt werden: Sie müssen immer noch aufrufen erase(), um diese Elemente tatsächlich aus dem Container zu entfernen. Also movebewegt removesich nicht , entfernt sich nicht. Ich hätte den Namen mark_movable()für gewählt move.
Ali
4
@Ali würde ich auch mark_movable()verwirrend finden . Es deutet darauf hin, dass es eine dauerhafte Nebenwirkung gibt, bei der es tatsächlich keine gibt.
Finnw

Antworten:

177

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 movemanchmal die Leute verwirrt. Die Absicht dieser Benennung ist jedoch nicht zu verwirren, sondern Ihren Code lesbarer zu machen.

Die Geschichte von movegeht 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 swapes 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 swapwirklich 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 moveist 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, movefragte 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:

Taxonomie

(Bild schamlos von dirkgent gestohlen )

Wenn wir heute swapgenau 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 movenichts 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::movebesitzt 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_xvalueund 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 movealso der generierte Objektcode?

Bedenken Sie Folgendes test:

void
test(int& i, int& j)
{
    i = j;
}

Kompiliert mit clang++ -std=c++14 test.cpp -O3 -Serzeugt 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::movehat nichts anderes getan, jals einen r-Wert umzuwandeln, und dann Xbindet 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++filtzeigt , dass X::operator=(X&&)statt aufgerufen wirdX::operator=(X const&) .

Und das ist alles was es zu std::movetun gibt ! Es verschwindet zur Laufzeit vollständig. Die einzige Auswirkung ist die Kompilierungszeit, in der möglicherweise geändert wird, welche Überladung aufgerufen wird.

Howard Hinnant
quelle
7
Hier ist eine Punktquelle für dieses Diagramm: Ich habe es digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }für das Gemeinwohl neu erstellt :) Laden Sie es hier als SVG
herunter
7
Ist das noch offen für Bikeshedding? Ich würde vorschlagen allow_move;)
Dyp
2
@dyp Mein Favorit ist immer noch movable.
Daniel Frey
6
Scott Meyers schlug vor std::move, rvalue_cast
umzubenennen
6
Da sich rvalue jetzt sowohl auf prvalues ​​als auch auf xvalues ​​bezieht, rvalue_castist seine Bedeutung nicht eindeutig: Welche Art von rvalue gibt es zurück? xvalue_castwäre hier ein einheitlicher Name. Leider würden die meisten Menschen zu diesem Zeitpunkt auch nicht verstehen, was es tut. In ein paar Jahren wird meine Aussage hoffentlich falsch.
Howard Hinnant
20

Lassen Sie mich hier nur ein Zitat aus den 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 rWert behandeln". Vielleicht wäre es besser gewesen, wenn move () rval () genannt worden wäre, aber mittlerweile wird move () seit Jahren verwendet.

Übrigens, ich habe die FAQ wirklich genossen - es lohnt sich zu lesen.

podkova
quelle
2
Um den Kommentar von @ HowardHinnant aus einer anderen Antwort zu plagiieren: Die Antwort von Stroustrup ist ungenau, da es jetzt zwei Arten von r-Werten gibt - prvalues ​​und xvalues, und std :: move ist wirklich eine xvalue-Besetzung.
Einpoklum