web-dev-qa-db-fra.com

Pourquoi `std :: move` est-il nommé` std :: move`?

La fonction C++ 11 std::move(x) ne bouge vraiment rien du tout. C'est juste une conversion en valeur r. Pourquoi cela at-il été fait? N'est-ce pas trompeur?

119
Howard Hinnant

Il est vrai que std::move(x) n'est qu'un transtypage en rvalue - plus précisément en xvalue, par opposition à --- prvalue . Et il est également vrai qu'avoir un cast nommé move dé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 move remonte à la proposition de déplacement d'origine en 2002 . Cet article présente d'abord la référence rvalue, puis montre comment écrire un std::swap Plus efficace:

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 "&&" Pourrait éventuellement signifier était logique et. Personne ne connaissait les références rvalue, ni les implications de transtyper une lvalue en une rvalue (tout en ne faisant pas de copie comme le ferait static_cast<T>(t)). Les lecteurs de ce code penseraient donc naturellement:

Je sais comment swap est censé fonctionner (copier sur temporaire puis échanger les valeurs), mais quel est le but de ces vilaines conversions?!

Notez également que swap est vraiment juste un remplaçant pour toutes sortes d'algorithmes de modification de permutation. Cette discussion est much, beaucoup plus grande que swap.

Ensuite, la proposition introduit sucre syntaxique qui remplace le static_cast<T&&> Par quelque chose de plus lisible qui ne transmet pas le quoi, mais plutôt le pourquoi =:

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

C'est à dire. move n'est que du sucre syntaxique pour static_cast<T&&>, et maintenant le code est assez suggestif pour expliquer pourquoi ces transtypages sont là: pour activer la sémantique des mouvements!

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 de l'expliquer):

La sémantique de déplacement entre automatiquement en jeu lorsque des arguments rvalue sont donnés. Ceci est parfaitement sûr car le déplacement de ressources depuis une rvalue ne peut pas être remarqué par le reste du programme (personne d'autre n'a une référence à la rvalue afin de 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 à rvalue?


Le point principal:

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

Mais pourquoi déménagez-vous?


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

Taxonomy

(image volée sans vergogne à dirkgently )

Et donc aujourd'hui, si nous voulions que swap dise précisément ce que 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);
}

En tout état de cause, le programmeur C++ compagnon devrait 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 déplacer à partir des rhs, par opposition à copier à partir 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::move Ne 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, 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, 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++ évolue constamment).

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, 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 du tout ​​dans le code objet. On peut généraliser ce résultat à: Pour déplaçables trivialement ​​objets, std::move N'a aucun impact.

Voyons 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_ À c++filt, Cela 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 y a toujours aucun changement du tout ​​dans le code objet généré. std::move N'a fait que convertir j en une rvalue, puis cette 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 change 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

L'exécution de __ZN1XaSEOS_ À c++filt Révèle que X::operator=(X&&) est appelée au lieu 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 est au moment de la compilation où il pourrait ​​modifier ce que la surcharge est appelée.

165
Howard Hinnant

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 valeur r". Peut-être que cela aurait été mieux si move () avait été appelé rval (), mais à présent move () est utilisé depuis des années.

Au fait, j'ai vraiment apprécié le FAQ - ça vaut le coup d'être lu.

19
podkova