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 :
(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.
0 votes
Pour aggraver les choses, les trois arguments
std::move
En fait, ça bouge0 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 appelererase()
pour retirer réellement ces éléments du conteneur. Doncmove
ne bouge pas,remove
ne s'enlève pas. J'aurais choisi le nommark_movable()
paramove
.4 votes
@Ali je trouverais
mark_movable()
confus également. Il suggère qu'il y a un effet secondaire durable alors qu'en fait il n'y en a pas.