Pourquoi `std :: move` s'appelle` std :: move`?

128

La fonction C ++ 11 std::move(x)ne déplace vraiment rien du tout. C'est juste un casting à la valeur r. Pourquoi cela a-t-il été fait? N'est-ce pas trompeur?

Howard Hinnant
la source
Pour aggraver les choses, les trois arguments std::movebougent en fait ..
Cubbi
Et n'oubliez pas le C ++ std::char_traits::move
98/03/11
30
Mon autre favori est celui std::remove()qui ne supprime pas les éléments: vous devez toujours appeler erase()pour supprimer réellement ces éléments du conteneur. Donc movene bouge pas, removene supprime pas. J'aurais choisi le nom mark_movable()pour move.
Ali
4
@Ali je trouverais mark_movable()aussi déroutant. Cela suggère qu'il y a un effet secondaire durable là où il n'y en a pas.
finnw

Réponses:

178

Il est correct que ce std::move(x)soit juste une conversion en rvalue - plus spécifiquement en une xvalue , par opposition à une prvalue . Et il est également vrai qu'avoir un casting nommé movedéroute parfois les gens. Cependant, l'intention de cette dénomination n'est pas de confondre, mais plutôt de rendre votre code plus lisible.

L'histoire de moveremonte à la proposition de déménagement d'origine en 2002 . Cet article présente d'abord la référence rvalue, puis montre comment écrire une méthode plus efficace 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);
}

Il faut se rappeler qu'à ce stade de l'histoire, la seule chose que « &&» pouvait signifier était logique et . Personne n'était familier avec les références rvalue, ni avec les implications de transtyper une lvalue en rvalue (sans faire une copie comme le static_cast<T>(t)ferait). Les lecteurs de ce code penseraient donc naturellement:

Je sais comment swapest censé fonctionner (copier en temporaire puis échanger les valeurs), mais à quoi servent ces vilains moulages?!

Notez également qu'il ne swaps'agit en réalité que d'un substitut pour toutes sortes d'algorithmes de modification de permutation. Cette discussion est beaucoup , beaucoup plus grande que swap.

Ensuite, la proposition introduit le sucre de syntaxe qui remplace le static_cast<T&&>par quelque chose de plus lisible qui ne transmet pas le quoi précis , mais plutôt le pourquoi :

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

Ie moveest juste du sucre de syntaxe pour static_cast<T&&>, et maintenant le code est assez évocateur quant à la raison pour laquelle ces transtypages sont là: pour activer la sémantique de déplacement!

Il faut comprendre que dans le contexte de l'histoire, peu de gens à ce stade ont vraiment compris le lien intime entre les valeurs et la sémantique du mouvement (bien que l'article essaie également d'expliquer cela):

La sémantique de déplacement entrera automatiquement en jeu lorsque des arguments rvalue seront donnés. Ceci est parfaitement sûr car le déplacement des ressources d'une rvalue ne peut pas être remarqué par le reste du programme ( personne d'autre n'a de référence à la rvalue pour détecter une différence ).

Si à l'époque swapétait plutôt présenté comme ceci:

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);
}

Ensuite, les gens auraient regardé cela et auraient dit:

Mais pourquoi jetez-vous à la valeur?


Le point principal:

En fait, en utilisant move, personne n'a jamais demandé:

Mais pourquoi déménagez-vous?


Au fil des années et de l'affinement de la proposition, les notions de lvaleur et de rvalue ont été affinées dans les catégories de valeur que nous avons aujourd'hui:

Taxonomie

( l' image sans honte volée à dirkgently )

Et donc aujourd'hui, si nous voulions swapdire précisément ce qu'il fait, au lieu de pourquoi , cela devrait ressembler davantage à:

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);
}

Et la question que tout le monde devrait se poser est de savoir si le code ci-dessus est plus ou moins lisible que:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

Ou même l'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);
}

Dans tous les cas, le programmeur C ++ compagnon doit savoir que sous le capot de move, il ne se passe rien de plus qu'un casting. Et le programmeur C ++ débutant, au moins avec move, sera informé que l'intention est de passer des rhs, par opposition à copier des rhs, même s'ils ne comprennent pas exactement comment cela est accompli.

De plus, si un programmeur désire cette fonctionnalité sous un autre nom, std::movene possède aucun monopole sur cette fonctionnalité, et aucune magie de langage non portable n'est impliquée dans son implémentation. Par exemple, si l'on voulait coder set_value_category_to_xvalue, et l'utiliser à la place, c'est trivial de le faire:

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);
}

En C ++ 14, cela devient encore plus concis:

template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<std::remove_reference_t<T>&&>(t);
}

Donc, si vous êtes si enclin, décorez votre static_cast<T&&>comme bon vous semble, et vous finirez peut-être par développer une nouvelle meilleure pratique (C ++ est en constante évolution).

Alors, que fait move-on en termes de code objet généré?

Considérez ceci test:

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

Compilé avec clang++ -std=c++14 test.cpp -O3 -S, cela produit ce code objet:

__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

Maintenant, si le test est changé en:

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

Il n'y a absolument aucun changement dans le code objet. On peut généraliser ce résultat à: Pour les objets trivialement mobiles , std::moven'a aucun impact.

Regardons maintenant cet exemple:

struct X
{
    X& operator=(const X&);
};

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

Cela génère:

__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

Si vous exécutez à __ZN1XaSERKS_travers c++filtelle produit: X::operator=(X const&). Pas de surprise ici. Maintenant, si le test est changé en:

void
test(X& i, X& j)
{
    i = std::move(j);
}

Ensuite, il n'y a toujours aucun changement dans le code objet généré. std::moven'a rien fait d'autre jqu'un transtypage en rvalue, puis cette rvalue Xse lie à l'opérateur d'affectation de copie de X.

Ajoutons maintenant un opérateur d'affectation de déplacement à X:

struct X
{
    X& operator=(const X&);
    X& operator=(X&&);
};

Maintenant , le code objet ne change:

__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

Courir à __ZN1XaSEOS_travers c++filtrévèle que X::operator=(X&&)est appelé au lieu de X::operator=(X const&).

Et c'est tout ce qu'il y a à faire std::move! Il disparaît complètement au moment de l'exécution. Son seul impact est au moment de la compilation où il peut modifier l'appel de la surcharge.

Howard Hinnant
la source
7
Voici une source de points pour ce graphique: Je recréée digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }pour le bien public :) Téléchargez ici SVG
sehe
7
Est-ce toujours ouvert au vélo? Je suggérerais allow_move;)
dyp
2
@dyp Mon préféré est toujours movable.
Daniel Frey
6
Scott Meyers a suggéré de renommer std::moveà rvalue_cast: youtube.com/...
nairware
6
Puisque rvalue fait maintenant référence à la fois aux prvalues ​​et aux xvalues, rvalue_castson sens est ambigu: quel type de rvalue renvoie-t-il? xvalue_castserait un nom cohérent ici. Malheureusement, la plupart des gens, à l'heure actuelle, ne comprendraient pas non plus ce qu'il fait. J'espère que dans quelques années, ma déclaration deviendra fausse.
Howard Hinnant
20

Permettez-moi de laisser ici une citation de la FAQ C ++ 11 écrite par B.Stroustrup, qui est une réponse directe à la question d'OP:

move (x) signifie "vous pouvez traiter x comme une rvalue". Peut-être que cela aurait été mieux si move () avait été appelé rval (), mais maintenant move () est utilisé depuis des années.

Au fait, j'ai vraiment apprécié la FAQ - cela vaut la peine d'être lu.

Podkova
la source
2
Pour plagier le commentaire de @ HowardHinnant à partir d'une autre réponse: la réponse de Stroustrup est inexacte, car il existe maintenant deux types de rvalues ​​- prvalues ​​et xvalues, et std :: move est en fait un cast de xvalue.
einpoklum