Quel est l'avantage d'utiliser des références de transfert dans des boucles for basées sur des plages?

115

const auto&suffirait si je veux effectuer des opérations en lecture seule. Cependant, je suis tombé sur

for (auto&& e : v)  // v is non-const

quelques fois récemment. Cela me fait me demander:

Est-il possible que, dans certains cas obscurs, l'utilisation de références de transfert présente un avantage en termes de performances par rapport à auto&ou const auto&?

( shared_ptrest un suspect pour les cas de coin obscurs)


Mettre à jour Deux exemples que j'ai trouvés dans mes favoris:

Un inconvénient de l'utilisation de la référence const lors de l'itération sur les types de base?
Puis-je facilement parcourir les valeurs d'une carte à l'aide d'une boucle for basée sur une plage?

Veuillez vous concentrer sur la question: pourquoi voudrais-je utiliser les boucles auto && basées sur la plage pour les boucles?

Ali
la source
4
Le voyez-vous vraiment "souvent"?
Courses de légèreté en orbite
2
Je ne suis pas sûr qu'il y ait suffisamment de contexte dans votre question pour que je puisse évaluer à quel point c'est «fou» là où vous le voyez.
Courses de légèreté en orbite
2
@LightnessRacesinOrbit Longue histoire: pourquoi voudrais-je utiliser auto&&des boucles for basées sur la plage?
Ali

Réponses:

94

Le seul avantage que je peux voir est lorsque l'itérateur de séquence renvoie une référence proxy et que vous devez opérer sur cette référence d'une manière non const. Par exemple, considérez:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto& e : v)
        e = true;
}

Cela ne compile pas car rvalue vector<bool>::referencerenvoyée par iteratorne sera pas liée à une référence lvalue non const. Mais cela fonctionnera:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    for (auto&& e : v)
        e = true;
}

Cela étant dit, je ne coderais pas de cette façon à moins que vous ne sachiez que vous devez satisfaire un tel cas d'utilisation. C'est-à-dire que je ne ferais pas cela gratuitement parce que cela amène les gens à se demander ce que vous faites. Et si je le faisais, cela ne ferait pas de mal d'inclure un commentaire expliquant pourquoi:

#include <vector>

int main()
{
    std::vector<bool> v(10);
    // using auto&& so that I can handle the rvalue reference
    //   returned for the vector<bool> case
    for (auto&& e : v)
        e = true;
}

Éditer

Ce dernier cas devrait vraiment être un modèle pour avoir du sens. Si vous savez que la boucle gère toujours une référence proxy, autocela fonctionnera aussi bien que auto&&. Mais lorsque la boucle traitait parfois des références non proxy et parfois des références proxy, je pense que auto&&cela deviendrait la solution de choix.

Howard Hinnant
la source
3
D'un autre côté, il n'y a pas de désavantage explicite , n'est-ce pas? (Mis à part les personnes potentiellement déroutantes, ce qui, à mon avis, ne vaut pas la peine d'être mentionné, personnellement.)
ildjarn
7
Un autre terme pour écrire du code qui confond inutilement les gens est: écrire du code confusqué. Il est préférable de rendre votre code aussi simple que possible, mais pas plus simple. Cela aidera à réduire le nombre de bogues. Cela étant dit, au fur et à mesure que && deviendra plus familier, alors peut-être que dans 5 ans, les gens s'attendront à un idiome auto && (en supposant que cela ne fait pas de mal). Je ne sais pas si cela arrivera ou non. Mais le simple est dans l'œil du spectateur, et si vous écrivez pour plus que pour vous-même, tenez compte de vos lecteurs.
Howard Hinnant
7
Je préfère const auto&quand je veux que le compilateur m'aide à vérifier que je ne modifie pas accidentellement les éléments de la séquence.
Howard Hinnant
31
J'aime personnellement utiliser auto&&dans du code générique où j'ai besoin de modifier les éléments de la séquence. Si je ne le fais pas, je m'en tiendrai auto const&.
Xeo
7
@Xeo: +1 C'est grâce à des passionnés comme vous, qui expérimentent constamment et poussent pour de meilleures façons de faire les choses, que le C ++ continue d'évoluer. Je vous remercie. :-)
Howard Hinnant
26

L'utilisation de références universellesauto&& ou avec une boucle basée sur une plage présente l'avantage de capturer ce que vous obtenez. Pour la plupart des types d'itérateurs, vous obtiendrez probablement un ou un pour un type . Le cas intéressant est celui où le déréférencement d'un itérateur produit une valeur temporaire: C ++ 2011 a des exigences assouplies et les itérateurs ne sont pas nécessairement nécessaires pour produire une lvalue. L'utilisation de références universelles correspond à la transmission d'argument dans :forT&T const&Tstd::for_each()

template <typename InIt, typename F>
F std::for_each(InIt it, InIt end, F f) {
    for (; it != end; ++it) {
        f(*it); // <---------------------- here
    }
    return f;
}

L'objet de la fonction fpeut traiter T&, T const&et Tdifféremment. Pourquoi le corps d'une forboucle basée sur une plage devrait-il être différent? Bien sûr, pour profiter réellement d'avoir déduit le type à l'aide de références universelles, vous devez les transmettre en conséquence:

for (auto&& x: range) {
    f(std::forward<decltype(x)>(x));
}

Bien sûr, utiliser std::forward()signifie que vous acceptez toutes les valeurs renvoyées à partir de. Je ne sais pas (encore?) Si des objets comme celui-ci ont beaucoup de sens dans un code non-modèle. J'imagine que l'utilisation de références universelles peut offrir plus d'informations au compilateur pour faire la bonne chose. Dans le code basé sur des modèles, il ne prend aucune décision sur ce qui devrait se passer avec les objets.

Dietmar Kühl
la source
9

J'utilise pratiquement toujours auto&&. Pourquoi se faire mordre par un étui de bord alors que ce n'est pas nécessaire? C'est plus court à taper aussi, et je le trouve simplement plus ... transparent. Lorsque vous utilisez auto&& x, vous savez que xc'est exactement *it, à chaque fois.

Chiot
la source
29
Mon problème est que vous constrenoncez à l'être avec auto&&si cela const auto&suffit. La question demande les cas d'angle où je peux me faire mordre. Quels sont les cas de coin qui n'ont pas encore été mentionnés par Dietmar ou Howard?
Ali
2
Voici un moyen de se faire mordre si vous faites usage auto&&. Si le type que vous capturez doit être déplacé dans le corps de la boucle (par exemple), et est changé à une date ultérieure afin qu'il se résout en un const&type, votre code continuera de fonctionner en silence, mais vos mouvements seront des copies. Ce code sera très trompeur. Cependant, si vous spécifiez explicitement le type comme référence de valeur r, quiconque a changé le type de conteneur obtiendra une erreur de compilation car vous vouliez vraiment que ces objets
soient
@cyberbisson Je viens de tomber sur ceci: comment puis-je appliquer une référence rvalue sans spécifier explicitement le type? Quelque chose comme for (std::decay_t<decltype(*v.begin())>&& e: v)? Je suppose qu'il y a une meilleure façon ...
Jerry Ma
@JerryMa Cela dépend de ce que vous ne savez sur le type. Comme mentionné ci-dessus, auto&&donnera une "référence universelle", donc toute autre chose que cela vous donnera un type plus spécifique. Si vest supposé être a vector, vous pouvez le faire decltype(v)::value_type&&, ce que je suppose que vous vouliez en prenant le résultat de operator*sur un type d'itérateur. Vous pouvez également decltype(begin(v))::value_type&&vérifier les types de l'itérateur au lieu du conteneur. Si nous avons si peu d'informations sur le type, cependant, nous pourrions considérer qu'il est un peu plus clair d'aller avec auto&&...
cyberbisson