133 votes

Pourquoi `std::move` s'appelle-t-il `std::move` ?

Le C++11 std::move(x) La fonction ne fait pas vraiment bouger quoi que ce soit. Il s'agit simplement d'un transfert vers la valeur r. Pourquoi cela a-t-il été fait ? N'est-ce pas trompeur ?

0 votes

Pour aggraver les choses, les trois arguments std::move En fait, ça bouge

0 votes

Et n'oubliez pas le C++98/03/11. std::char_traits::move :-)

32 votes

Mon autre favori est std::remove() qui ne supprime pas les éléments : Vous devez toujours appeler erase() pour retirer réellement ces éléments du conteneur. Donc move ne bouge pas, remove ne s'enlève pas. J'aurais choisi le nom mark_movable() para move .

183voto

Howard Hinnant Points 59526

Il est exact que std::move(x) est juste un transfert vers rvalue - plus spécifiquement vers un xvalue par opposition à un prvalue . Et il est également vrai que le fait d'avoir un casting nommé move confond parfois les gens. Cependant, l'intention de ce nommage n'est pas de confondre, mais plutôt de rendre votre code plus lisible.

L'histoire de move remonte à la proposition initiale de déménagement en 2002 . Cet article présente tout d'abord la référence rvalue, puis montre comment écrire une référence rvalue 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 moment de l'histoire, la seule chose qui " && "pourrait signifier que logique et . Personne n'était familier avec les références rvalue, ni avec les implications de la transformation d'une lvalue en une rvalue (sans faire de copie comme le ferait une static_cast<T>(t) ferait). Donc les lecteurs de ce code penseraient naturellement :

Je sais comment swap est censé fonctionner (copier en temporaire puis échanger les valeurs), mais à quoi servent ces vilains castings !

Notez également que swap n'est en fait qu'un substitut pour toutes sortes d'algorithmes de modification de permutation. Cette discussion est beaucoup beaucoup plus grand que swap .

Ensuite, la proposition introduit sucre de syntaxe qui remplace le static_cast<T&&> avec quelque chose de plus lisible qui transmet non pas la précision ce que mais plutôt le pourquoi :

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

I.e. move n'est que du sucre de syntaxe pour static_cast<T&&> Le code est maintenant assez suggestif quant à la raison pour laquelle ces casts sont là : pour permettre la sémantique de déplacement !

Il faut comprendre que dans le contexte de l'histoire, peu de gens à ce moment-là ont vraiment compris le lien intime entre les valeurs r et la sémantique des mouvements (bien que l'article tente d'expliquer cela aussi) :

La sémantique du mouvement entre automatiquement en jeu lorsqu'on lui donne des arguments rvalue des arguments. Ceci est parfaitement sûr car le déplacement des ressources d'une valeur rvalue ne peut pas être remarqué par le reste du programme ( personne d'autre n'a une référence à la valeur r afin de détecter une différence ).

Si au moment 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);
}

Alors les gens auraient regardé ça et auraient dit :

Mais pourquoi tu fais un casting vers rvalue ?


Le point principal :

Comme c'était le cas, en utilisant move personne n'a jamais demandé :

Mais pourquoi déménager ?


Au fil des années et de l'affinement de la proposition, les notions de lvalue et de rvalue ont été affinées pour donner naissance à la notion de catégories de valeurs que nous avons aujourd'hui :

Taxonomy

(image volée sans vergogne à dirkgently )

Et donc aujourd'hui, si nous voulions swap pour dire précisément ce que qu'il fait, au lieu de pourquoi il devrait plutôt ressembler à :

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

Quoi qu'il en soit, le programmeur C++ chevronné doit savoir que sous le capot de l'application move il n'y a rien de plus qu'une distribution. Et le programmeur C++ débutant, du moins avec move seront informés que l'intention est de déplacer de la rhs, par opposition à copie de la rhs, même s'ils ne comprennent pas exactement comment qui est accompli.

De plus, si un programmeur désire cette fonctionnalité sous un autre nom, std::move ne possède aucun monopole sur cette fonctionnalité, et il n'y a aucune magie de langage non portable impliquée dans sa mise en œuvre. Par exemple, si l'on voulait coder set_value_category_to_xvalue et l'utiliser à la place, il 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, il est 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 en avez envie, décorez votre static_cast<T&&> de la manière qui vous semble la plus appropriée, et peut-être finirez-vous par développer une nouvelle meilleure pratique (le C++ est en constante évolution).

Alors que fait move 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 ce qui 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 y a absolument aucun changement dans le code objet. On peut généraliser ce résultat à : Pour trivialement mobile objets, std::move n'a aucun impact.

Examinons 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_ par le biais de c++filt qu'il 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);
}

Alors il y a encore aucun changement dans le code objet généré. std::move n'a fait que lancer j à un rvalue, et ensuite ce rvalue X se 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 fait changement :

__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

Running __ZN1XaSEOS_ par le biais de c++filt révèle que X::operator=(X&&) est appelé à la place de X::operator=(X const&) .

Et c'est tout ce qu'il y a à std::move ! Il disparaît complètement au moment de l'exécution. Son seul impact se situe au moment de la compilation où elle pourrait modifie la surcharge qui est appelée.

1 votes

Même si c'est bien que nous puissions lui donner un autre nom, le reste du monde ne le reconnaîtra pas. :/

7 votes

Voici une source ponctuelle pour ce graphique : Je l'ai recréé digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } } pour le bien public :) Téléchargez-le ici comme SVG

7 votes

Est-ce que c'est toujours ouvert à la vente de vélos ? Je suggère allow_move ;)

20voto

podkova Points 711

Permettez-moi de laisser ici une citation de la FAQ C++11 écrit par B. Stroustrup, qui est une réponse directe à la question du PO :

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

Au fait, j'ai beaucoup apprécié la FAQ - elle vaut la peine d'être lue.

2 votes

Pour plagier le commentaire de @HowardHinnant dans 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 réalité un cast xvalue.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X