web-dev-qa-db-fra.com

Qu'est-ce que auto && nous dit?

Si vous lisez du code comme

auto&& var = foo();

foo est une fonction renvoyant une valeur de type T. Alors var est une lvalue de type rvalue référence à T. Mais qu'est-ce que cela implique pour var? Est-ce que cela signifie que nous sommes autorisés à voler les ressources de var? Y a-t-il des situations raisonnables où vous devriez utiliser auto&& pour dire au lecteur de votre code quelque chose comme vous le faites quand vous renvoyez un unique_ptr<> pour dire que vous avez la propriété exclusive? Et que dire par exemple T&& quand T est de type classe?

Je veux juste comprendre, s’il existe d’autres cas d’utilisation de auto&& que ceux de la programmation par modèle; comme ceux décrits dans les exemples de cet article Références universelles par Scott Meyers.

146
MWid

En utilisant auto&& var = <initializer> Vous dites: J'accepterai n'importe quel initialiseur, 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&&). Cela fonctionne parce qu’une "référence universelle", auto&& Ou T&&, Sera liée à n'importe quoi .

Vous pourriez dire, bien pourquoi ne pas simplement utiliser un const auto&, Car cela liera également à quelque chose? Le problème avec l'utilisation d'une référence const est que c'est const! Vous ne pourrez plus le lier ultérieurement à des références non constantes ni appeler des fonctions membres qui ne sont pas marquées const.

Par exemple, imaginons que vous souhaitiez obtenir un std::vector, Prenez un itérateur dans son premier élément et modifiez la valeur indiquée par cet itérateur:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Ce code se compilera parfaitement quelle que soit l'expression de l'initialiseur. Les alternatives à auto&& Échouent 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 de auto&& Comme ceci est dans une boucle for basée sur une plage. Voir mon autre question pour plus de détails.

Si vous utilisez ensuite std::forward Sur votre référence auto&& Pour conserver le fait qu’il s’agissait à l’origine d’une valeur ou d’une valeur, votre code indique: Si votre objet provient d'une expression lvalue ou rvalue, je souhaite préserver la valeur qu'il avait à l'origine afin de pouvoir l'utiliser de manière plus efficace, ce qui pourrait l'invalider.

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 déchirer ses tripes pour des raisons de performance (éviter les copies) lorsque l’initialiseur d’origine était une rvalue modifiable.

Qu'est-ce que cela signifie si nous pouvons ou quand nous pouvons voler des ressources de var? Eh bien, puisque le auto&& Sera lié à quelque chose, nous ne pouvons absolument pas essayer de déchirer vars nous-mêmes - cela peut très bien être une valeur lvalue ou même une constante Nous pouvons cependant std::forward Le faire pour d’autres fonctions qui pourraient totalement en ravager l’intérieur. Dès que nous faisons cela, considérons que var est dans un état invalide.

Appliquons maintenant ceci au cas de auto&& var = foo();, comme indiqué dans votre question, où foo renvoie un T par valeur. 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 valeur, nous n'avons pas besoin de la permission de std::forward Pour voler ses ressources. Dans ce cas spécifique, sachant que foo retourne par valeur , le lecteur doit le lire comme suit: je prends une référence rvalue au temporaire renvoyé de foo, afin que je puisse facilement m'en passer.


En tant qu'additif, je pense qu'il est utile de mentionner le moment où une expression telle que some_expression_that_may_be_rvalue_or_lvalue Peut apparaître, autre qu'une situation "bien, votre code pourrait changer". Alors, voici 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;
}

Ici, get_vector<T>() est cette belle expression qui pourrait être une valeur ou une valeur, en fonction du type générique T. Nous changeons essentiellement le type de retour de get_vector Par le paramètre template de foo.

Lorsque nous appelons foo<std::vector<int>>, get_vector Renvoie global_vec Sous forme de valeur, ce qui donne une expression rvalue. Alternativement, lorsque nous appelons foo<std::vector<int>&>, get_vector Renverra global_vec Par référence, ce qui donnera 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 le résultat suivant, comme prévu:

2
1
2
2

Si vous deviez remplacer le code auto&& Dans le code par auto, auto&, const auto& Ou const auto&&, Nous avons gagné " t obtenir le résultat que nous voulons.


Une autre façon de changer la logique du programme selon que votre référence auto&& 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
}
197
Joseph Mansfield

Premièrement, je recommande de lire ma réponse à cette question en tant que lecture parallèle pour une explication pas à pas du fonctionnement de la déduction d’argument de gabarit pour les références universelles.

Est-ce que cela signifie que nous sommes autorisés à voler les ressources de var?

Pas nécessairement. Que faire si foo() a tout à coup renvoyé une référence, ou si vous avez changé l'appel mais avez oublié de mettre à jour l'utilisation de var? Ou si vous êtes dans un code générique et que le type de retour de foo() peut changer en fonction de vos paramètres?

Pensez que auto&& Est exactement le même que le T&& Dans template<class T> void f(T&& v);, parce que c'est (presque) exactement ça. Que faites-vous avec les références universelles dans les fonctions, lorsque vous devez les transmettre ou 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. S'il s'agissait d'une lvalue avant d'être passé à votre fonction, il reste une lvalue après avoir été passé par std::forward. S'il s'agissait d'une valeur, elle redeviendra une valeur (rappelez-vous qu'une référence à une valeur nommée est une valeur).

Alors, comment utilisez-vous correctement var 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 var est un T&&, Vous obtiendrez une valeur de retour, et s'il s'agit de T&, Vous obtiendrez une valeur de retour.

Donc, revenons à la question: Que nous disent auto&& v = f(); et std::forward<decltype(v)>(v) dans une base de code? Ils nous disent que v sera acquis et transmis de la manière la plus efficace. Rappelez-vous qu'après avoir transmis une telle variable, il est possible qu'il soit déplacé de, il serait donc incorrect de l'utiliser ultérieurement sans le réinitialiser.

Personnellement, j'utilise auto&& Dans le code générique lorsque j'ai besoin d'une variable modifiable. La transmission parfaite d'une valeur est en train de changer, car l'opération de déplacement lui vole potentiellement les entrailles. Si je veux juste être paresseux (c.-à-d. Ne pas épeler le nom du type même si je le connais) et que je n'ai pas besoin de le modifier (par exemple, simplement pour imprimer des éléments d'une plage), je m'en tiendrai à auto const&.


auto est tellement différent que auto v = {1,2,3}; Fera de v un std::initializer_list, Tandis que f({1,2,3}) sera un échec de la déduction.

11
Xeo

Considérons un type T qui a un constructeur de mouvement, et supposons

T t( foo() );

utilise ce constructeur de mouvement.

Utilisons maintenant une référence intermédiaire pour capturer le retour de foo:

auto const &ref = foo();

ceci exclut l'utilisation du constructeur move, donc la valeur de retour devra être copiée au lieu d'être déplacée (même si nous utilisons std::move ici, nous ne pouvons pas réellement passer par une référence constante)

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 déménagement est toujours disponible.


Et pour répondre à vos autres questions:

... Existe-t-il des situations raisonnables dans lesquelles vous devriez utiliser auto && pour indiquer quelque chose au lecteur de votre code ...

La première chose, comme le dit Xeo, est essentiellement je passe X aussi efficacement que possible, quel que soit le type X. Donc, voir le code qui utilise auto&& en interne doit 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 êtes le propriétaire exclusif ...

Lorsqu'un modèle de fonction prend un argument de type T&&, cela signifie que vous pouvez déplacer l'objet que vous transmettez. Renvoyer unique_ptr donne explicitement la propriété à l'appelant; accepter T&& peut retirer propriété de l'appelant (si un acteur de déplacement existe, etc.).

2
Useless