2012-11-17 3 views
16

A C++Next blog post a dit queretour par référence rvalue

A compute(…) 
{ 
    A v; 
    … 
    return v; 
} 

Si A a une copie accessible ou constructeur déplacer, le compilateur peut choisir de elide la copie. Sinon, si A a un constructeur de déplacement, v est déplacé. Sinon, si A a un constructeur de copie, v est copié. Sinon, une erreur de compilation est émise.

Je pensais que je devrais toujours retourner la valeur sans std::move parce que le compilateur serait en mesure de déterminer le meilleur choix pour les utilisateurs. Mais dans un autre exemple du blog Le post

Matrix operator+(Matrix&& temp, Matrix&& y) 
    { temp += y; return std::move(temp); } 

Ici, le std::move est nécessaire parce que y doit être traité comme une lvalue dans la fonction.

Ah, ma tête a failli exploser après avoir étudié ce blog. J'ai fait de mon mieux pour comprendre le raisonnement mais plus j'étudiais, plus je devenais confus. Pourquoi devrions-nous retourner la valeur avec l'aide de std::move?

+1

Avez-vous un lien vers l'article en question? –

+1

[related FAQ] (http://stackoverflow.com/questions/3106110/) – fredoverflow

+1

http://cpp-next.com/archive/2009/09/making-your-next-move/, c'est une série à propos de La référence rvalue – StereoMatching

Répondre

21

Donc, disons que vous avez:

A compute() 
{ 
    A v; 
    … 
    return v; 
} 

Et vous faites:

A a = compute(); 

Il y a deux transferts (copier ou déplacer) qui sont impliqués dans cette expression. D'abord, l'objet noté v dans la fonction doit être transféré au résultat de la fonction, c'est-à-dire la valeur donnée par l'expression compute(). Appelons ce transfert 1. Ensuite, cet objet temporaire est transféré pour créer l'objet a - Transfert 2.

Dans de nombreux cas, les deux Transfer 1 et 2 peuvent être élidés par le compilateur - l'objet v est construit directement à l'emplacement de a et aucun transfert n'est nécessaire. Le compilateur doit utiliser l'optimisation de la valeur de retour nommée pour le transfert 1 dans cet exemple, car l'objet renvoyé est nommé. Cependant, si nous désactivons l'élision copier/déplacer, chaque transfert implique un appel au constructeur de copie de A ou à son constructeur de déplacement. Dans la plupart des compilateurs modernes, le compilateur verra que v est sur le point d'être détruit et il le déplacera d'abord dans la valeur de retour. Ensuite, cette valeur de retour temporaire sera déplacée vers a. Si A n'a pas de constructeur de déplacement, il sera copié pour les deux transferts à la place.

Maintenant, regardons:

A compute(A&& v) 
{ 
    return v; 
} 

La valeur que nous sommes de retour provient de la référence étant passé dans la fonction. Le compilateur ne suppose pas simplement que v est temporaire et qu'il est correct de le faire . Dans ce cas, le transfert 1 sera une copie. Ensuite, Transfer 2 sera un mouvement - c'est correct car la valeur retournée est toujours temporaire (nous n'avons pas retourné de référence). Mais puisque nous savons que nous avons fait un objet que nous pouvons passer de, parce que notre paramètre est une référence rvalue, nous pouvons dire explicitement au compilateur de traiter v comme temporaire std::move:

A compute(A&& v) 
{ 
    return std::move(v); 
} 

maintenant les deux Transfer 1 et Transfer 2 seront des mouvements.


La raison pour laquelle le compilateur ne traite pas automatiquement v, définie comme A&&, comme rvalue est une question de sécurité. Ce n'est pas seulement trop bête pour le comprendre. Une fois qu'un objet a un nom, il peut être référencé plusieurs fois dans votre code. Tenir compte:

A compute(A&& a) 
{ 
    doSomething(a); 
    doSomethingElse(a); 
} 

Si a a été automatiquement considérée comme une rvalue, doSomething serait libre de déchirer ses tripes, ce qui signifie que le a étant passé à doSomethingElse peut être invalide. Même si doSomething prenait son argument en valeur, l'objet serait déplacé et donc invalide dans la ligne suivante. Pour éviter ce problème, les références rvalue nommées sont lvalues. Cela signifie que lorsque doSomething est appelé, a sera au pire copié à partir de, si ce n'est pas simplement pris par référence lvalue - il sera toujours valide dans la ligne suivante.

Il appartient à l'auteur de compute de dire, "ok, maintenant je permets de déplacer cette valeur, car je sais avec certitude qu'il s'agit d'un objet temporaire". Vous faites cela en disant std::move(a). Par exemple, vous pourriez donner doSomething une copie, puis laisser doSomethingElse de passer de celui-ci:

A compute(A&& a) 
{ 
    doSomething(a); 
    doSomethingElse(std::move(a)); 
} 
+0

Voulez-vous dire que le compilateur peut implicitement déplacer ou élider automatiquement un objet? Dans le second cas, nous devrions utiliser std :: move pour aider le compilateur à comprendre que && v est un rvalue car notre compilateur mister n'est pas assez intelligent pour savoir Vous n'avez plus besoin de v? – StereoMatching

+0

Eh bien, la vérité est qu'une référence rvalue nommée est une expression lvalue. L'expression 'v' est une lvalue, même si son type est une référence rvalue. Ce n'est pas que le compilateur n'est pas assez intelligent - il serait dangereux pour lui de le traiter comme une valeur. Je vais ajouter un peu à ma réponse pour expliquer pourquoi. –

+0

Merci, je sens que je sais ce qu'est la référence rvalue "encore une fois".Cette chose est assez compliquée, je me demande si cette fonctionnalité ne serait utilisée que par les développeurs de bibliothèques et les fournisseurs de compilateurs. – StereoMatching

5

Un déplacement implicite des résultats de la fonction n'est possible que pour les objets automatiques. Un paramètre de référence rvalue ne désigne pas un objet automatique, vous devez donc demander explicitement le déplacement dans ce cas.

+0

Le point clé est que 'v' dans le premier exemple est détruit de toute façon à la fin de la fonction, il est donc logique que la dernière opération soit un mouvement plutôt qu'une copie. Cependant, dans le second exemple, l'objet référencé par 'temp' survivra après la fin de la fonction. –

4

Le premier tire parti de NVRO, ce qui est encore mieux que le déplacement. n ° copie est mieux que pas cher.

Le second ne peut pas tirer parti de NVRO. En supposant aucune élision, return temp; appelait le constructeur de copie et return std::move(temp); appelait le constructeur de déplacement. Maintenant, je crois que l'un ou l'autre d'entre eux a le même potentiel d'être élidé et donc vous devriez aller avec le moins cher si pas élidé, qui utilise std::move.

+2

L'appel de 'std :: move' inhibera NRVO, puisque l'expression de l'instruction return n'est plus" le nom d'un objet automatique non-volatile "(pas que' temp' était déjà un nom, mais de toute façon). – Xeo

Questions connexes