Si vous lisez du code comme
auto&& var = foo();
où foo
est toute fonction retournant par valeur de type T
. Alors var
est une lvalue de type rvalue référence à T
. Mais qu'est-ce que cela implique var
? Cela signifie-t-il que nous sommes autorisés à voler les ressources de var
? Existe-t-il des situations raisonnables où vous devriez utiliser auto&&
pour informer le lecteur de votre code quelque chose comme vous le faites lorsque vous retournez un unique_ptr<>
pour indiquer que vous en êtes la propriété exclusive? Et qu'en est-il par exemple T&&
quand T
est de type classe?
Je veux juste comprendre, s'il existe d'autres cas d'utilisation auto&&
que ceux de la programmation de modèles; comme ceux discutés dans les exemples de cet article Références universelles de Scott Meyers.
auto&&
? J'ai réfléchi à la raison pour laquelle une boucle for basée sur une plage se développe pour être utiliséeauto&&
comme exemple, mais je n'y suis pas parvenu. Peut-être que quiconque répond peut l'expliquer.foo
retours, en stockant une rvalue ref qui ressemble à UB à ne.foo
peut par exemple ressembler à :int foo(){return 1;}
.Réponses:
En utilisant,
auto&& var = <initializer>
vous dites: j'accepterai n'importe quel initialiseur indépendamment du fait qu'il s'agisse d'une expression lvalue ou rvalue et je conserverai sa constance . Ceci est généralement utilisé pour le transfert (généralement avecT&&
). La raison pour laquelle cela fonctionne est qu'une "référence universelle",auto&&
ouT&&
, se liera à n'importe quoi .Vous pourriez dire, pourquoi ne pas simplement utiliser un
const auto&
parce que cela se liera également à n'importe quoi? Le problème avec l'utilisation d'uneconst
référence est que c'estconst
! Vous ne pourrez pas le lier ultérieurement à des références non const ou appeler des fonctions membres non marquéesconst
.À titre d'exemple, imaginez que vous vouliez obtenir un
std::vector
, prenez un itérateur vers son premier élément et modifiez la valeur pointée par cet itérateur d'une manière ou d'une autre:Ce code compilera très bien quelle que soit l'expression d'initialisation. Les alternatives pour
auto&&
échouer des manières suivantes:Donc pour cela,
auto&&
fonctionne parfaitement! Un exemple d'utilisationauto&&
comme celui-ci est dans unefor
boucle basée sur une plage . Voir mon autre question pour plus de détails.Si vous utilisez ensuite
std::forward
sur votreauto&&
référence pour préserver le fait qu'il s'agissait à l'origine d'une lvalue ou d'une rvalue, votre code dit: Maintenant que j'ai votre objet à partir d'une expression lvalue ou rvalue, je veux conserver la valeur d'origine eu pour que je puisse l'utiliser plus efficacement - cela pourrait l'invalider. Un péché:Cela permet
use_it_elsewhere
de lui arracher les tripes pour des raisons de performances (en évitant les copies) lorsque l'initialiseur d'origine était une rvalue modifiable.Qu'est-ce que cela signifie pour savoir si nous pouvons ou quand nous pouvons voler des ressources
var
? Eh bien, puisque leauto&&
sera lié à quoi que ce soit, nous ne pouvons pas essayer de nous arrachervar
nous-mêmes les tripes - cela peut très bien être une lvalue ou même const. On peut cependant lestd::forward
faire à d'autres fonctions qui risquent de ravager totalement son intérieur. Dès que nous faisons cela, nous devrions considérer commevar
étant dans un état invalide.Appliquons maintenant ceci au cas de
auto&& var = foo();
, comme indiqué dans votre question, où foo renvoie uneT
valeur par. Dans ce cas, nous savons avec certitude que le type devar
sera déduit commeT&&
. Puisque nous savons avec certitude que c'est une rvalue, nous n'avons pas besoin destd::forward
la permission de voler ses ressources. Dans ce cas précis, sachant que celafoo
renvoie par valeur , le lecteur devrait simplement le lire comme suit: je prends une référence rvalue au temporaire renvoyéfoo
, donc je peux m'en éloigner avec plaisir.En guise d'addendum, je pense qu'il vaut la peine de mentionner quand une expression comme
some_expression_that_may_be_rvalue_or_lvalue
pourrait apparaître, autre qu'une situation «et bien votre code pourrait changer». Voici donc un exemple artificiel:Voici
get_vector<T>()
cette belle expression qui pourrait être une lvalue ou une rvalue selon le type génériqueT
. Nous changeons essentiellement le type de retour deget_vector
via le paramètre de modèle defoo
.Lorsque nous appelons
foo<std::vector<int>>
,get_vector
retourneraglobal_vec
par valeur, ce qui donne une expression rvalue. Alternativement, lorsque nous appelonsfoo<std::vector<int>&>
,get_vector
retourneraglobal_vec
par référence, résultant en une expression lvalue.Si nous faisons:
Nous obtenons la sortie suivante, comme prévu:
Si vous deviez changer
auto&&
dans le code à l' un desauto
,auto&
,const auto&
ouconst auto&&
alors nous n'obtiendrons pas le résultat que nous voulons.Une autre façon de modifier la logique du programme selon que votre
auto&&
référence est initialisée avec une expression lvalue ou rvalue consiste à utiliser des traits de type:la source
T vec = get_vector<T>();
intérieur de la fonction foo? Ou est-ce que je le simplifie à un niveau absurde :)Tout d'abord, je recommande de lire ma réponse en tant que lecture parallèle pour une explication étape par étape sur le fonctionnement de la déduction d'arguments de modèle pour les références universelles.
Pas nécessairement. Et si
foo()
tout à coup renvoyé une référence, ou si vous avez changé l'appel mais que vous avez oublié de mettre à jour l'utilisation devar
? Ou si vous êtes en code générique et que le type de retour defoo()
peut changer en fonction de vos paramètres?Pensez
auto&&
à être exactement le même que leT&&
intemplate<class T> void f(T&& v);
, car c'est (presque † ) exactement cela. Que faites-vous des références universelles dans les fonctions, lorsque vous avez besoin de les transmettre ou de les utiliser de quelque manière que ce soit? Vous utilisezstd::forward<T>(v)
pour récupérer la catégorie de valeur d'origine. Si c'était une lvalue avant d'être passée à votre fonction, elle reste une lvalue après avoir été passéestd::forward
. Si c'était une rvalue, elle deviendra à nouveau une rvalue (rappelez-vous, une référence rvalue nommée est une lvalue).Alors, comment utilisez-vous
var
correctement de manière générique? Utilisezstd::forward<decltype(var)>(var)
. Cela fonctionnera exactement de la même manière questd::forward<T>(v)
dans le modèle de fonction ci-dessus. Sivar
est aT&&
, vous récupérerez une rvalue, et si c'est le casT&
, vous récupérerez une lvalue.Alors, revenons sur le sujet: que nous disent
auto&& v = f();
etstd::forward<decltype(v)>(v)
dans une base de code? Ils nous disent quev
cela sera acquis et transmis de la manière la plus efficace. Rappelez-vous, cependant, qu'après avoir transféré une telle variable, il est possible qu'elle soit déplacée, il serait donc incorrect de l'utiliser davantage sans la réinitialiser.Personnellement, j'utilise
auto&&
en code générique quand j'ai besoin d'une variable modifiable . La transmission parfaite d'une rvalue est en train de changer, car l'opération de déplacement vole potentiellement ses tripes. Si je veux juste être paresseux (c'est-à-dire ne pas épeler le nom du type même si je le connais) et ne pas avoir besoin de le modifier (par exemple, lorsque je n'imprime que des éléments d'une plage), je m'en tiendrai àauto const&
.†
auto
est tellement différent queauto v = {1,2,3};
cela ferav
unstd::initializer_list
, alors que cef({1,2,3})
sera un échec de déduction.la source
foo()
renvoie un type valeurT
, alorsvar
(cette expression) sera une lvalue et son type (de cette expression) sera une référence rvalue àT
(ieT&&
).Considérez un type
T
qui a un constructeur de mouvement et supposezutilise ce constructeur de déplacement.
Maintenant, utilisons une référence intermédiaire pour capturer le retour de
foo
:cela exclut l'utilisation du constructeur de mouvement, donc la valeur de retour devra être copiée au lieu d'être déplacée (même si nous l'utilisons
std::move
ici, nous ne pouvons pas réellement nous déplacer à travers une référence const)Cependant, si nous utilisons
le constructeur de mouvement est toujours disponible.
Et pour répondre à vos autres questions:
La première chose, comme le dit Xeo, est essentiellement que je passe X aussi efficacement que possible , quel que soit le type X. Ainsi, voir du code qui utilise en
auto&&
interne devrait indiquer qu'il utilisera la sémantique de déplacement en interne le cas échéant.Lorsqu'un modèle de fonction prend un argument de type
T&&
, il indique qu'il peut déplacer l'objet que vous passez. Le retourunique_ptr
donne explicitement la propriété à l'appelant; l'acceptationT&&
peut retirer la propriété de l'appelant (si un cteur de déplacement existe, etc.).la source
ref
etrvref
sont tous les deux des valeurs l. Si vous voulez le constructeur de mouvement, vous devez écrireT t(std::move(rvref))
.auto const &
:?auto&&
et que dites-vous au lecteur de votre code en utilisantauto&&
?La
auto &&
syntaxe utilise deux nouvelles fonctionnalités de C ++ 11:La
auto
partie permet au compilateur de déduire le type en fonction du contexte (la valeur de retour dans ce cas). Ceci est sans aucune qualification de référence (vous permettant de spécifier si vous voulezT
,T &
ouT &&
pour un type déduitT
).C'est
&&
la nouvelle sémantique du mouvement. Un type prenant en charge la sémantique de déplacement implémente un constructeurT(T && other)
qui déplace de manière optimale le contenu dans le nouveau type. Cela permet à un objet d'échanger la représentation interne au lieu d'effectuer une copie complète.Cela vous permet d'avoir quelque chose comme:
Alors:
effectuera une copie du vecteur retourné (cher), mais:
va permuter la représentation interne du vecteur (le vecteur de
foo
et le vecteur vide devar
), ce sera donc plus rapide.Ceci est utilisé dans la nouvelle syntaxe de la boucle for:
Où la boucle for contient un
auto &&
à la valeur de retour defoo
etitem
est une référence à chaque valeur dansfoo
.la source
auto&&
ne bougera rien, il fera juste une référence. Qu'il s'agisse d'une référence lvalue ou rvalue dépend de l'expression utilisée pour l'initialiser.std::vector
etstd::string
sont moveconstructibles. Cela n'a rien à voir avec le type devar
.