Que nous dit auto &&?

168

Si vous lisez du code comme

auto&& var = foo();

fooest toute fonction retournant par valeur de type T. Alors varest 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 Test 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.

MWid
la source
1
Je me suis demandé la même chose. Je comprends comment fonctionne la déduction de type, mais ce qui est mon code dire quand je l' utilise auto&&? J'ai réfléchi à la raison pour laquelle une boucle for basée sur une plage se développe pour être utilisée auto&&comme exemple, mais je n'y suis pas parvenu. Peut-être que quiconque répond peut l'expliquer.
Joseph Mansfield
1
Est-ce même légal? Je veux dire que l'instance de T est détruite immédiatement après les fooretours, en stockant une rvalue ref qui ressemble à UB à ne.
1
@aleguna C'est parfaitement légal. Je ne veux pas renvoyer une référence ou un pointeur vers une variable locale, mais une valeur. La fonction foopeut par exemple ressembler à : int foo(){return 1;}.
MWid
9
Les références @aleguna aux temporaires exécutent une extension de durée de vie, comme dans C ++ 98.
ecatmur
5
L'extension de vie @aleguna ne fonctionne qu'avec les temporaires locaux, pas les fonctions renvoyant des références. Voir stackoverflow.com/a/2784304/567292
ecatmur

Réponses:

232

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 constré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 forboucle basée sur une plage . Voir mon autre question pour plus de détails.

Si vous utilisez ensuite std::forwardsur 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_elsewherede 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 varnous-mêmes les tripes - cela peut très bien être une lvalue ou même const. On peut cependant le std::forwardfaire à 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 Tvaleur par. Dans ce cas, nous savons avec certitude que le type de varsera déduit comme T&&. Puisque nous savons avec certitude que c'est une rvalue, nous n'avons pas besoin de std::forwardla permission de voler ses ressources. Dans ce cas précis, sachant que cela foorenvoie 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_lvaluepourrait 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_vectorvia le paramètre de modèle de foo.

Lorsque nous appelons foo<std::vector<int>>, get_vectorretournera global_vecpar valeur, ce qui donne une expression rvalue. Alternativement, lorsque nous appelons foo<std::vector<int>&>, get_vectorretournera global_vecpar 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
}
Joseph Mansfield
la source
2
Ne pouvons-nous pas simplement dire à l' T vec = get_vector<T>();intérieur de la fonction foo? Ou est-ce que je le simplifie à un niveau absurde :)
Asterisk
@Asterisk Non bcoz T vec ne peut être assigné à lvalue que dans le cas de std :: vector <int &> et si T est std :: vector <int> alors nous utiliserons l'appel par valeur, ce qui est inefficace
Kapil
1
auto & me donne le même résultat. J'utilise MSVC 2015. Et GCC produit une erreur.
Sergey Podobry
Ici, j'utilise MSVC 2015, auto & donne les mêmes résultats que celui de auto &&.
Kehe CAI
Pourquoi int i; auto && j = i; autorisé mais int i; int && j = i; n'est pas ?
SeventhSon84 le
14

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.

Cela signifie-t-il que nous sommes autorisés à voler les ressources de var?

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 de var? Ou si vous êtes en code générique et que le type de retour de foo()peut changer en fonction de vos paramètres?

Pensez auto&&à être exactement le même que le T&&in template<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 utilisez std::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ée std::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 varcorrectement de manière générique? Utilisez std::forward<decltype(var)>(var). Cela fonctionnera exactement de la même manière que std::forward<T>(v)dans le modèle de fonction ci-dessus. Si varest a T&&, vous récupérerez une rvalue, et si c'est le cas T&, vous récupérerez une lvalue.

Alors, revenons sur le sujet: que nous disent auto&& v = f();et std::forward<decltype(v)>(v)dans une base de code? Ils nous disent que vcela 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&.


autoest tellement différent que auto v = {1,2,3};cela fera vun std::initializer_list, alors que ce f({1,2,3})sera un échec de déduction.

Xeo
la source
Dans la première partie de votre réponse: je veux dire que si foo()renvoie un type valeur T, alors var(cette expression) sera une lvalue et son type (de cette expression) sera une référence rvalue à T(ie T&&).
MWid
@MWid: Cela a du sens, a supprimé la première partie.
Xeo
3

Considérez un type Tqui a un constructeur de mouvement et supposez

T t( foo() );

utilise ce constructeur de déplacement.

Maintenant, utilisons une référence intermédiaire pour capturer le retour de foo:

auto const &ref = 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::moveici, nous ne pouvons pas réellement nous déplacer à travers une référence const)

T t(std::move(ref));   // invokes T::T(T const&)

Cependant, si nous utilisons

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

le constructeur de mouvement est toujours disponible.


Et pour répondre à vos autres questions:

... Y a-t-il des situations raisonnables où vous devriez utiliser auto && pour informer le lecteur de votre code quelque chose ...

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.

... comme vous le faites lorsque vous renvoyez un unique_ptr <> pour indiquer que vous avez la propriété exclusive ...

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 retour unique_ptrdonne explicitement la propriété à l'appelant; l'acceptation T&&peut retirer la propriété de l'appelant (si un cteur de déplacement existe, etc.).

Inutile
la source
2
Je ne suis pas sûr que votre deuxième exemple soit valide. N'avez-vous pas besoin d'une transmission parfaite pour invoquer le constructeur de mouvement?
3
C'est faux. Dans les deux cas, le constructeur de copie est appelé, puisque refet rvrefsont tous les deux des valeurs l. Si vous voulez le constructeur de mouvement, vous devez écrire T t(std::move(rvref)).
MWid
Vouliez-vous dire const ref dans votre premier exemple auto const &:?
PiotrNycz
@aleguna - vous et MWid avez raison, merci. J'ai corrigé ma réponse.
Inutile
1
@Useless Vous avez raison. Mais cela ne répond pas à ma question. Quand utilisez-vous auto&&et que dites-vous au lecteur de votre code en utilisant auto&&?
MWid
-3

La auto &&syntaxe utilise deux nouvelles fonctionnalités de C ++ 11:

  1. La autopartie 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 voulez T, T &ou T &&pour un type déduit T).

  2. C'est &&la nouvelle sémantique du mouvement. Un type prenant en charge la sémantique de déplacement implémente un constructeur T(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:

std::vector<std::string> foo();

Alors:

auto var = foo();

effectuera une copie du vecteur retourné (cher), mais:

auto &&var = foo();

va permuter la représentation interne du vecteur (le vecteur de fooet le vecteur vide de var), ce sera donc plus rapide.

Ceci est utilisé dans la nouvelle syntaxe de la boucle for:

for (auto &item : foo())
    std::cout << item << std::endl;

Où la boucle for contient un auto &&à la valeur de retour de fooet itemest une référence à chaque valeur dans foo.

reece
la source
Ceci est une erreur. 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.
Joseph Mansfield
Dans les deux cas, le constructeur de mouvement sera appelé, puisque std::vectoret std::stringsont moveconstructibles. Cela n'a rien à voir avec le type de var.
MWid
1
@MWid: En fait, l'appel au constructeur de copie / déplacement pourrait également être complètement élidé avec RVO.
Matthieu M.
@MatthieuM. Vous avez raison. Mais je pense que dans l'exemple ci-dessus, le cnstructor de copie ne sera jamais appelé, puisque tout est moveconstructible.
MWid
1
@MWid: mon point était que même le constructeur de mouvement peut être éludé. Elision l'emporte sur le mouvement (c'est moins cher).
Matthieu M.