Comment std :: move () transfère-t-il les valeurs dans RValues?

104

Je me suis juste retrouvé à ne pas comprendre pleinement la logique de std::move().

Au début, je l'ai recherché sur Google, mais il semble qu'il n'y ait que des documents sur la façon de l'utiliser std::move(), pas sur le fonctionnement de sa structure.

Je veux dire, je sais ce qu'est la fonction de membre du modèle, mais quand je regarde la std::move()définition dans VS2010, cela reste déroutant.

la définition de std :: move () va ci-dessous.

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
    move(_Ty&& _Arg)
    {   // forward _Arg as movable
        return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
    }

Ce qui m'est étrange en premier, c'est le paramètre (_Ty && _Arg), car lorsque j'appelle la fonction comme vous le voyez ci-dessous,

// main()
Object obj1;
Object obj2 = std::move(obj1);

cela équivaut fondamentalement à

// std::move()
_Ty&& _Arg = Obj1;

Mais comme vous le savez déjà, vous ne pouvez pas directement lier une LValue à une référence RValue, ce qui me fait penser qu'il devrait en être ainsi.

_Ty&& _Arg = (Object&&)obj1;

Cependant, c'est absurde car std :: move () doit fonctionner pour toutes les valeurs.

Donc, je suppose que pour bien comprendre comment cela fonctionne, je devrais également jeter un coup d'œil à ces structures.

template<class _Ty>
struct _Remove_reference
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{   // remove rvalue reference
    typedef _Ty _Type;
};

Malheureusement, c'est toujours aussi déroutant et je ne comprends pas.

Je sais que tout cela est dû à mon manque de compétences de base en syntaxe sur C ++. J'aimerais savoir comment ils fonctionnent à fond et tous les documents que je peux obtenir sur Internet seront plus que bienvenus. (Si vous pouvez simplement expliquer cela, ce sera génial aussi).

Dean Seo
la source
31
@NicolBolas Au contraire, la question elle-même montre qu'OP se sous-vende dans cette déclaration. C'est précisément la maîtrise par OP des compétences de base en syntaxe qui lui permet même de poser la question.
Kyle Strand
Je ne suis pas d'accord de toute façon - vous pouvez apprendre rapidement ou avoir déjà une bonne maîtrise de l'informatique ou de la programmation dans son ensemble, même en C ++, et toujours lutter avec la syntaxe. Il est préférable de passer votre temps à apprendre les mécanismes, les algorithmes, etc., mais s'appuyer sur des références pour rafraîchir la syntaxe au besoin, plutôt que d'être techniquement articulé mais d'avoir une compréhension superficielle de choses comme copier / avancer / avancer. Comment êtes-vous censé savoir "quand et comment utiliser std :: move lorsque vous voulez déplacer une construction" avant de savoir ce que fait le mouvement?
John P
Je pense que je suis venu ici pour comprendre comment movefonctionne plutôt que comment il est mis en œuvre. Je trouve cette explication vraiment utile: pagefault.blog/2018/03/01/… .
Anton Daneyko le

Réponses:

171

Nous commençons par la fonction de déplacement (que j'ai nettoyée un peu):

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

Commençons par la partie la plus simple - c'est-à-dire lorsque la fonction est appelée avec rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

et notre movemodèle est instancié comme suit:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

Puisque remove_referenceconvertit T&en Tou T&&en T, et Objectn'est pas une référence, notre fonction finale est:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

Maintenant, vous vous demandez peut-être: avons-nous même besoin du casting? La réponse est: oui, nous le faisons. La raison est simple; La référence nommée rvalue est traitée comme lvalue (et la conversion implicite de lvalue en référence rvalue est interdite par la norme).


Voici ce qui se passe lorsque nous appelons moveavec lvalue:

Object a; // a is lvalue
Object b = std::move(a);

et moveinstanciation correspondante :

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Encore une fois, remove_referenceconvertis Object&à Objectet nous obtenons:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

Maintenant, nous arrivons à la partie délicate: que signifie Object& &&même et comment peut-il se lier à lvalue?

Pour permettre un transfert parfait, la norme C ++ 11 fournit des règles spéciales pour la réduction des références, qui sont les suivantes:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

Comme vous pouvez le voir, sous ces règles Object& &&signifie en fait Object&, qui est une simple référence lvalue qui permet de lier les lvalues.

La fonction finale est donc:

Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

ce qui n'est pas différent de l'instanciation précédente avec rvalue - ils convertissent tous les deux son argument en rvalue reference puis le renvoient. La différence est que la première instanciation ne peut être utilisée qu'avec rvalues, tandis que la seconde fonctionne avec lvalues.


Pour expliquer pourquoi nous avons besoin d' remove_referenceun peu plus, essayons cette fonction

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

et instanciez-le avec lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

En appliquant les règles de réduction de référence mentionnées ci-dessus, vous pouvez voir que nous obtenons une fonction qui est inutilisable move(pour le dire simplement, vous l'appelez avec lvalue, vous récupérez lvalue). Si quoi que ce soit, cette fonction est la fonction d'identité.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Vitus
la source
3
Bonne réponse. Bien que je comprenne que pour une lvalue, c'est une bonne idée de l'évaluer Tcomme Object&, je ne savais pas que c'était vraiment fait. Je me serais attendu Tà évaluer également Objectdans ce cas, car je pensais que c'était la raison de l'introduction de références de wrapper et std::ref, ou non.
Christian Rau
2
Il y a une différence entre template <typename T> void f(T arg)(ce qui est dans l'article de Wikipedia) et template <typename T> void f(T& arg). Le premier se résout en valeur (et si vous voulez passer une référence, vous devez l'envelopper std::ref), tandis que le second se résout toujours en référence. Malheureusement, les règles pour la déduction des arguments de modèle sont plutôt complexes, je ne peux donc pas fournir de raisonnement précis sur les raisons pour lesquelles il T&&résout Object& &&(mais cela arrive effectivement).
Vitus le
1
Mais y a-t-il une raison pour laquelle cette approche directe ne fonctionnerait pas? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo
1
Cela explique pourquoi remove_reference est nécessaire, mais je ne comprends toujours pas pourquoi la fonction doit prendre T && (au lieu de T &). Si je comprends bien cette explication, peu importe que vous obteniez un T && ou un T & car de toute façon, remove_reference le castera en T, puis vous rajouterez le &&. Alors pourquoi ne pas dire que vous acceptez un T & (ce qui est sémantiquement ce que vous acceptez), au lieu de T && et que vous vous appuyez sur un transfert parfait pour permettre à l'appelant de passer un T &?
mgiuca
3
@mgiuca: Si vous ne voulez std::moveconvertir que des lvalues ​​en rvalues, alors oui, ce T&serait bien. Cette astuce est faite principalement pour la flexibilité: vous pouvez appeler std::movetout (rvalues ​​inclus) et récupérer rvalue.
Vitus
4

_Ty est un paramètre de modèle, et dans cette situation

Object obj1;
Object obj2 = std::move(obj1);

_Ty est de type "Objet &"

c'est pourquoi la _Remove_reference est nécessaire.

Ce serait plus comme

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

Si nous ne supprimions pas la référence, ce serait comme nous le faisions

Object&& obj2 = (ObjectRef&&)obj1_ref;

Mais ObjectRef && se réduit à Object &, que nous n'avons pas pu lier à obj2.

La raison pour laquelle il réduit de cette manière est de soutenir une transmission parfaite. Voir cet article .

Vaughn Cato
la source
Cela n'explique pas tout pourquoi le _Remove_reference_est nécessaire. Par exemple, si vous avez Object&comme typedef et que vous y faites référence, vous obtenez toujours Object&. Pourquoi cela ne fonctionne-t-il pas avec &&? Il y a une réponse à cela, et cela a à voir avec une transmission parfaite.
Nicol Bolas
2
Vrai. Et la réponse est très intéressante. A & && est réduit à A &, donc si nous essayions d'utiliser (ObjectRef &&) obj1_ref, nous obtiendrions Object & à la place dans ce cas.
Vaughn Cato le