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é 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é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 swap
est censé fonctionner (copier en temporaire puis échanger les valeurs), mais à quoi servent ces vilains moulages?!
Notez également qu'il ne swap
s'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 move
est 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:
( l' image sans honte volée à dirkgently )
Et donc aujourd'hui, si nous voulions swap
dire 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::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, 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::move
n'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++filt
elle 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::move
n'a rien fait d'autre j
qu'un transtypage en 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 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++filt
ré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.
std::move
bougent en fait ..