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 avec T&&
). La raison pour laquelle cela fonctionne est qu'une "référence universelle", auto&&
ou T&&
, 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'une const
référence est que c'est const
! Vous ne pourrez pas le lier ultérieurement à des références non const ou appeler des fonctions membres non marquées const
.
À 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:
auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;
Ce code compilera très bien quelle que soit l'expression d'initialisation. Les alternatives pour auto&&
échouer des manières suivantes:
auto => will copy the vector, but we wanted a reference
auto& => will only bind to modifiable lvalues
const auto& => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues
Donc pour cela, auto&&
fonctionne parfaitement! Un exemple d'utilisation auto&&
comme celui-ci est dans une for
boucle basée sur une plage . Voir mon autre question pour plus de détails.
Si vous utilisez ensuite std::forward
sur votre auto&&
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é:
auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));
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 le auto&&
sera lié à quoi que ce soit, nous ne pouvons pas essayer de nous arracher var
nous-mêmes les tripes - cela peut très bien être une lvalue ou même const. On peut cependant le std::forward
faire à d'autres fonctions qui risquent de ravager totalement son intérieur. Dès que nous faisons cela, nous devrions considérer comme var
étant dans un état invalide.
Appliquons maintenant ceci au cas de auto&& var = foo();
, comme indiqué dans votre question, où foo renvoie une T
valeur par. Dans ce cas, nous savons avec certitude que le type de var
sera déduit comme T&&
. Puisque nous savons avec certitude que c'est une rvalue, nous n'avons pas besoin de std::forward
la permission de voler ses ressources. Dans ce cas précis, sachant que cela foo
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:
std::vector<int> global_vec{1, 2, 3, 4};
template <typename T>
T get_vector()
{
return global_vec;
}
template <typename T>
void foo()
{
auto&& vec = get_vector<T>();
auto i = std::begin(vec);
(*i)++;
std::cout << vec[0] << std::endl;
}
Voici get_vector<T>()
cette belle expression qui pourrait être une lvalue ou une rvalue selon le type générique T
. Nous changeons essentiellement le type de retour de get_vector
via le paramètre de modèle de foo
.
Lorsque nous appelons foo<std::vector<int>>
, get_vector
retournera global_vec
par valeur, ce qui donne une expression rvalue. Alternativement, lorsque nous appelons foo<std::vector<int>&>
, get_vector
retournera global_vec
par référence, résultant en une expression lvalue.
Si nous faisons:
foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;
Nous obtenons la sortie suivante, comme prévu:
2
1
2
2
Si vous deviez changer auto&&
dans le code à l' un des auto
, auto&
, const auto&
ou const 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:
if (std::is_lvalue_reference<decltype(var)>::value) {
// var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
// var was initialised with an rvalue expression
}
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.