228 votes

Quelle est la manière correcte d'utiliser la méthode de l'intervalle pour C++11 ?

Quelle est la manière correcte d'utiliser la fonction de plage de C++11 ? for ?

Quelle syntaxe faut-il utiliser ? for (auto elem : container) , ou for (auto& elem : container) o for (const auto& elem : container) ? Ou d'autres ?

7 votes

Les mêmes considérations que pour les arguments de fonction s'appliquent.

3 votes

En fait, cela n'a pas grand-chose à voir avec la portée pour. On peut dire la même chose de tout auto (const)(&) x = <expr>; .

3 votes

@MatthieuM : Cela a lot à faire avec la gamme basée pour, bien sûr ! Prenons le cas d'un débutant qui voit plusieurs syntaxes et ne peut pas choisir la forme à utiliser. Le but des "Q&R" était d'essayer de faire la lumière, et d'expliquer les différences de certains cas (et de discuter des cas qui compilent bien mais qui sont plutôt inefficaces à cause de copies profondes inutiles, etc.)

432voto

Mr.C64 Points 11681

TL;DR : Considérez les directives suivantes :

  1. Pour en observant les éléments, utilisez la syntaxe suivante :

    for (const auto& elem : container)    // capture by const reference
    • Si les objets sont bon marché à copier (comme int s, double s, etc.), il est possible d'utiliser une forme légèrement simplifiée :

        for (auto elem : container)    // capture by value
  2. Pour modification de les éléments en place, utiliser :

    for (auto& elem : container)    // capture by (non-const) reference
    • Si le conteneur utilise "itérateurs proxy" (comme std::vector<bool> ), utiliser :

        for (auto&& elem : container)    // capture by &&

Bien sûr, s'il y a un besoin de faire un copie locale de l'élément à l'intérieur du corps de la boucle, en capturant par valeur ( for (auto elem : container) ) est un bon choix.


Discussion détaillée

Commençons par faire la différence entre en observant les éléments du conteneur contre modification de les mettre en place.

Observer les éléments

Prenons un exemple simple :

vector<int> v = {1, 3, 5, 7, 9};

for (auto x : v)
    cout << x << ' ';

Le code ci-dessus imprime les éléments ( int ) dans le vector :

1 3 5 7 9

Considérons maintenant un autre cas, dans lequel les éléments du vecteur ne sont pas de simples entiers, mais des instances d'une classe plus complexe, avec un constructeur de copie personnalisé, etc.

// A sample test class, with custom copy semantics.
class X
{
public:
    X() 
        : m_data(0) 
    {}

    X(int data)
        : m_data(data)
    {}

    ~X() 
    {}

    X(const X& other) 
        : m_data(other.m_data)
    { cout << "X copy ctor.\n"; }

    X& operator=(const X& other)
    {
        m_data = other.m_data;       
        cout << "X copy assign.\n";
        return *this;
    }

    int Get() const
    {
        return m_data;
    }

private:
    int m_data;
};

ostream& operator<<(ostream& os, const X& x)
{
    os << x.Get();
    return os;
}

Si nous utilisons la méthode ci-dessus for (auto x : v) {...} syntaxe avec cette nouvelle classe :

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}

le résultat est quelque chose comme :

[... copy constructor calls for vector<X> initialization ...]

Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9

Comme on peut le lire sur la sortie, constructeur de copie sont effectués pendant les itérations de la boucle for basée sur l'intervalle.
C'est parce que nous sommes capturer les éléments du conteneur par valeur (le auto x partie en for (auto x : v) ).

C'est inefficace par exemple, si ces éléments sont des instances de std::string , des allocations de mémoire de tas peuvent être effectuées, avec des déplacements coûteux vers le gestionnaire de mémoire, etc. Ceci est inutile si nous voulons simplement observez les éléments d'un conteneur.

Une meilleure syntaxe est donc disponible : capture par const référence c'est-à-dire const auto& :

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (const auto& x : v)
{ 
    cout << x << ' ';
}

Maintenant la sortie est :

 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

Sans appel intempestif (et potentiellement coûteux) au constructeur de copie.

Alors, quand en observant éléments dans un conteneur (c'est-à-dire pour un accès en lecture seule), la syntaxe suivante convient pour de simples cheap-to-copy comme int , double etc :

for (auto elem : container) 

Sinon, capturer par const La référence est meilleure dans le cas général , afin d'éviter les appels inutiles (et potentiellement coûteux) au constructeur de copie :

for (const auto& elem : container) 

Modifier les éléments du conteneur

Si nous voulons modifier les éléments d'un conteneur en utilisant la méthode des plages for , les for (auto elem : container) y for (const auto& elem : container) les syntaxes sont fausses.

En fait, dans le premier cas, elem stocke un copie de l'élément d'origine, de sorte que les modifications qui lui sont apportées sont simplement perdues et ne sont pas stockées de manière persistante dans le conteneur. dans le conteneur, par exemple :

vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)  // <-- capture by value (copy)
    x *= 10;      // <-- a local temporary copy ("x") is modified,
                  //     *not* the original vector element.

for (auto x : v)
    cout << x << ' ';

La sortie est juste la séquence initiale :

1 3 5 7 9

Au lieu de cela, une tentative d'utiliser for (const auto& x : v) ne parvient pas à compiler.

g++ affiche un message d'erreur semblable à celui-ci :

TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
          x *= 10;
            ^

Dans ce cas, l'approche correcte consiste à capturer par des moyens autres que l'électricité. const référence :

vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
    x *= 10;

for (auto x : v)
    cout << x << ' ';

Le résultat est (comme prévu) :

10 30 50 70 90

Ce site for (auto& elem : container) La syntaxe fonctionne également pour des types plus complexes, par exemple, en considérant un vector<string> :

vector<string> v = {"Bob", "Jeff", "Connie"};

// Modify elements in place: use "auto &"
for (auto& x : v)
    x = "Hi " + x + "!";

// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
    cout << x << ' ';

le résultat est :

Hi Bob! Hi Jeff! Hi Connie!

Le cas particulier des itérateurs proxy

Supposons que nous ayons un vector<bool> et nous voulons inverser l'état logique booléen de ses éléments, en utilisant la syntaxe ci-dessus :

vector<bool> v = {true, false, false, true};
for (auto& x : v)
    x = !x;

Le code ci-dessus ne se compile pas.

g++ affiche un message d'erreur similaire à celui-ci :

TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
 type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
     for (auto& x : v)
                    ^

Le problème est que std::vector Le modèle est spécialisé pour bool avec une mise en œuvre qui paquets le site bool s pour optimiser l'espace (chaque valeur booléenne est stockée dans un bit, soit huit bits "booléens" dans un octet).

Pour cette raison (puisqu'il n'est pas possible de renvoyer une référence à un seul bit), vector<bool> utilise une méthode dite "itérateur de proxy" modèle. Un "proxy iterator" est un itérateur qui, lorsqu'il est déréférencé, fait no donner un ordinaire bool & mais renvoie à la place (par valeur) un objet temporaire , qui est un classe proxy convertible en bool . (Voir aussi cette question et les réponses associées ici sur StackOverflow).

Pour modifier en place les éléments de vector<bool> un nouveau type de syntaxe (utilisant auto&& ) doit être utilisé :

for (auto&& x : v)
    x = !x;

Le code suivant fonctionne bien :

vector<bool> v = {true, false, false, true};

// Invert boolean status
for (auto&& x : v)  // <-- note use of "auto&&" for proxy iterators
    x = !x;

// Print new element values
cout << boolalpha;        
for (const auto& x : v)
    cout << x << ' ';

et les sorties :

false true true false

Notez que le for (auto&& elem : container) La syntaxe fonctionne également dans les autres cas d'itérateurs ordinaires (non proxy) (par exemple pour un vector<int> ou un vector<string> ).

(Soit dit en passant, la syntaxe "observation" susmentionnée de la fonction for (const auto& elem : container) fonctionne bien aussi pour le cas de l'itérateur de proxy).

Résumé

La discussion ci-dessus peut être résumée dans les lignes directrices suivantes :

  1. Pour en observant les éléments, utilisez la syntaxe suivante :

    for (const auto& elem : container)    // capture by const reference
    • Si les objets sont bon marché à copier (comme int s, double s, etc.), il est possible d'utiliser une forme légèrement simplifiée :

        for (auto elem : container)    // capture by value
  2. Pour modification de les éléments en place, utiliser :

    for (auto& elem : container)    // capture by (non-const) reference
    • Si le conteneur utilise "itérateurs proxy" (comme std::vector<bool> ), utiliser :

        for (auto&& elem : container)    // capture by &&

Bien sûr, s'il y a un besoin de faire un copie locale de l'élément à l'intérieur du corps de la boucle, en capturant par valeur ( for (auto elem : container) ) est un bon choix.


Notes supplémentaires sur le code générique

Sur code générique puisque nous ne pouvons pas faire d'hypothèses sur le type générique T être bon marché à copier, en en observant il est prudent de toujours utiliser for (const auto& elem : container) .
(Cela ne déclenchera pas de copies inutiles potentiellement coûteuses, et fonctionnera très bien pour les types de copies bon marché tels que int et également pour les conteneurs utilisant des itérateurs proxy, tels que std::vector<bool> .)

En outre, dans modification de si nous voulons code générique pour fonctionner également dans le cas de proxy-itérateurs, la meilleure option consiste à for (auto&& elem : container) .
(Cela fonctionnera aussi très bien pour les conteneurs utilisant des itérateurs non-proxy ordinaires, comme par exemple std::vector<int> o std::vector<string> .)

Ainsi, en code générique les lignes directrices suivantes peuvent être fournies :

  1. Pour en observant les éléments, utiliser :

    for (const auto& elem : container)
  2. Pour modification de les éléments en place, utiliser :

    for (auto&& elem : container)

7 votes

Aucun conseil pour les contextes génériques ? :(

11 votes

Pourquoi ne pas toujours utiliser auto&& ? Existe-t-il un const auto&& ?

1 votes

Je suppose que vous ne voyez pas le cas où vous avez réellement besoin d'une copie à l'intérieur de la boucle ?

18voto

Il n'y a pas manière correcte à utiliser for (auto elem : container) ou for (auto& elem : container) o for (const auto& elem : container) . Vous exprimez simplement ce que vous voulez.

Permettez-moi de développer ce point. Faisons une promenade.

for (auto elem : container) ...

Celui-ci est du sucre syntaxique :

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

Vous pouvez l'utiliser si votre conteneur contient des éléments faciles à copier.

for (auto& elem : container) ...

Celui-ci est du sucre syntaxique :

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

Utilisez cette option lorsque vous souhaitez écrire directement dans les éléments du conteneur, par exemple.

for (const auto& elem : container) ...

Celui-ci est du sucre syntaxique :

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

Comme le dit le commentaire, juste pour la lecture. Et c'est à peu près tout, tout est "correct" lorsqu'il est utilisé correctement.

4voto

Puppy Points 90818

Le moyen correct est toujours

for(auto&& elem : container)

Cela garantira la préservation de toute la sémantique.

7 votes

Mais qu'en est-il si le conteneur ne renvoie que des références modifiables et que je veux indiquer clairement que je ne souhaite pas les modifier dans la boucle ? Ne devrais-je pas alors utiliser auto const & pour que mon intention soit claire ?

0 votes

@RedX : Qu'est-ce qu'une "référence modifiable" ?

0 votes

@LightnessRacesinOrbit int & ? Une référence normale, non constante.

1voto

R Sahu Points 24027

Bien que la motivation initiale de la boucle range-for ait pu être la facilité d'itération sur les éléments d'un conteneur, la syntaxe est suffisamment générique pour être utile même pour des objets qui ne sont pas purement des conteneurs.

L'exigence syntaxique pour la boucle for est que range_expression soutien begin() y end() soit comme des fonctions -- soit comme des fonctions membres du type sur lequel elle est évaluée, soit comme des fonctions non membres qui prennent une instance du type.

À titre d'exemple, on peut générer une plage de nombres et itérer sur cette plage à l'aide de la classe suivante.

struct Range
{
   struct Iterator
   {
      Iterator(int v, int s) : val(v), step(s) {}

      int operator*() const
      {
         return val;
      }

      Iterator& operator++()
      {
         val += step;
         return *this;
      }

      bool operator!=(Iterator const& rhs) const
      {
         return (this->val < rhs.val);
      }

      int val;
      int step;
   };

   Range(int l, int h, int s=1) : low(l), high(h), step(s) {}

   Iterator begin() const
   {
      return Iterator(low, step);
   }

   Iterator end() const
   {
      return Iterator(high, 1);
   }

   int low, high, step;
}; 

Avec les éléments suivants main fonction,

#include <iostream>

int main()
{
   Range r1(1, 10);
   for ( auto item : r1 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r2(1, 20, 2);
   for ( auto item : r2 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r3(1, 20, 3);
   for ( auto item : r3 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;
}

on obtiendrait le résultat suivant.

1 2 3 4 5 6 7 8 9 
1 3 5 7 9 11 13 15 17 19 
1 4 7 10 13 16 19

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X