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?
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:
(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.
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.