Quelle est la bonne façon d'utiliser la plage basée sur C ++ 11?


212

Quelle est la bonne façon d'utiliser la plage basée sur C ++ 11 for?

Quelle syntaxe utiliser? for (auto elem : container), ou for (auto& elem : container)ou for (const auto& elem : container)? Ou un autre?


6
La même considération s'applique que pour les arguments de fonction.
Maxim Egorushkin

3
En fait, cela n'a pas grand-chose à voir avec la plage basée sur. On peut en dire autant de tout auto (const)(&) x = <expr>;.
Matthieu M.

2
@MatthieuM: Cela a beaucoup à voir avec la gamme pour, bien sûr! Considérons un débutant qui voit plusieurs syntaxes et ne peut pas choisir la forme à utiliser. Le point de "Q & A" était d'essayer de faire la lumière et d'expliquer les différences de certains cas (et discuter des cas qui compilent bien mais sont un peu inefficaces en raison de copies profondes inutiles, etc.).
Mr.C64

2
@ Mr.C64: En ce qui me concerne, cela a plus à voir avec auto, en général, qu'avec la gamme basée sur; vous pouvez parfaitement l'utiliser sans aucune base auto! for (int i: v) {}est parfaitement bien. Bien sûr, la plupart des points que vous soulevez dans votre réponse peuvent avoir plus à voir avec le type qu'avec auto... mais d'après la question, il n'est pas clair où se situe le point douloureux. Personnellement, je me battrais pour retirer autode la question; ou peut-être explicite que si vous utilisez autoou nommez explicitement le type, la question se concentre sur la valeur / référence.
Matthieu M.

1
@MatthieuM .: Je suis ouvert à changer le titre ou à éditer la question sous une forme qui pourrait les rendre plus claires ... Encore une fois, mon objectif était de discuter de plusieurs options pour les syntaxes basées sur une plage (montrant le code qui compile mais est inefficace, code qui ne parvient pas à être compilé, etc.) et essayant d'offrir des conseils à quelqu'un (en particulier au niveau débutant) approchant les boucles basées sur la plage C ++ 11.
Mr.C64

Réponses:


390

Commençons à différencier l' observation des éléments du conteneur et leur modification sur 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 intdans vector:

1 3 5 7 9

Considérons maintenant un autre cas, dans lequel les éléments vectoriels ne sont pas seulement 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 for (auto x : v) {...}syntaxe ci-dessus avec cette nouvelle classe:

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

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

la sortie 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 il peut être lu à partir de la sortie, des appels de constructeur de copie sont effectués pendant les itérations de boucle basées sur la plage.
En effet, nous capturons les éléments du conteneur par valeur (la auto xpartie dans for (auto x : v)).

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

Ainsi, une meilleure syntaxe est disponible: capture par constréférence , c'est const auto&-à- dire :

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 de constructeur de copie parasite (et potentiellement coûteux).

Ainsi, lorsque l' observation des éléments dans un récipient ( par exemple pour l' accès en lecture seule), la syntaxe suivante est très bien pour de simples pas cher-à-copie types, comme int, double, etc .:

for (auto elem : container) 

Sinon, la capture par constréférence est meilleure dans le cas général , pour éviter les appels de constructeur de copie inutiles (et potentiellement coûteux):

for (const auto& elem : container) 

Modification des éléments du conteneur

Si nous voulons modifier les éléments d'un conteneur à l'aide de la plage for, ce qui précède for (auto elem : container)et les for (const auto& elem : container) syntaxes sont incorrects.

En fait, dans le premier cas, elemstocke une copie de l'élément d'origine, donc les modifications qui y sont apportées sont juste perdues et ne sont pas stockées de manière persistante 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 n'est que la séquence initiale:

1 3 5 7 9

Au lieu de cela, une tentative d'utilisation for (const auto& x : v)échoue simplement à la compilation.

g ++ affiche un message d'erreur quelque chose comme ceci:

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

L'approche correcte dans ce cas est la capture par non- constréférence:

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

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

La sortie est (comme prévu):

10 30 50 70 90

Cette for (auto& elem : container)syntaxe fonctionne également pour les types plus complexes, par exemple en considérant a 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 << ' ';

la sortie est:

Hi Bob! Hi Jeff! Hi Connie!

Le cas particulier des itérateurs proxy

Supposons que nous ayons un vector<bool>, et que nous voulons inverser l'état booléen logique 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 parvient pas à compiler.

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::vectorgabarit est spécialisé pour bool, avec une mise en oeuvre que des packs les bools à optimiser l' espace (chaque valeur booléenne est stockée dans un bit, huit bits « booléen » à un octet).

Pour cette raison (puisqu'il n'est pas possible de renvoyer une référence à un seul bit), vector<bool>utilise un modèle dit "itérateur proxy" . Un "itérateur proxy" est un itérateur qui, lorsqu'il est déréférencé, ne donne pas un objet ordinaire bool &, mais renvoie à la place (par valeur) un objet temporaire , qui est une classe proxy convertible enbool . (Voir également 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 correctement:

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 sorties:

false true true false

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

(En remarque, la syntaxe "d'observation" susmentionnée for (const auto& elem : container)fonctionne également pour le cas de l'itérateur proxy.)

Résumé

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

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

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

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

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

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

Bien sûr, s'il est nécessaire de faire une copie locale de l'élément à l'intérieur du corps de la boucle, la capture par value ( for (auto elem : container)) est un bon choix.


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

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

De plus, en mode modification , si nous voulons que le code générique fonctionne également dans le cas des proxy-itérateurs, la meilleure option est for (auto&& elem : container).
(Cela fonctionnera très bien également pour les conteneurs utilisant des itérateurs non proxy ordinaires, comme std::vector<int>ou std::vector<string>.)

Ainsi, dans le code générique , les directives suivantes peuvent être fournies:

  1. Pour observer les éléments, utilisez:

    for (const auto& elem : container)
  2. Pour modifier les éléments en place, utilisez:

    for (auto&& elem : container)

7
Aucun conseil pour les contextes génériques? :(
R. Martinho Fernandes

11
Pourquoi ne pas toujours utiliser auto&&? Y a-t-il un const auto&&?
Martin Ba

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

6
"Si le conteneur utilise des" itérateurs proxy "" - et vous savez qu'il utilise des "itérateurs proxy" (ce qui pourrait ne pas être le cas dans le code générique). Je pense donc que le meilleur est en effet auto&&, car il couvre auto&tout aussi bien.
Christian Rau

5
Merci, c'était une très bonne "introduction au cours intensif" de la syntaxe et quelques conseils pour la plage basée sur, pour un programmeur C #. +1.
AndrewJacksonZA

17

Il n'y a pas de bonne façon d'utiliser for (auto elem : container)ou for (auto& elem : container)ou for (const auto& elem : container). Vous exprimez simplement ce que vous voulez.

Permettez-moi de développer cela. Prenons une promenade.

for (auto elem : container) ...

Celui-ci est du sucre syntaxique pour:

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

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

}

Vous pouvez utiliser celui-ci si votre conteneur contient des éléments dont la copie est bon marché.

for (auto& elem : container) ...

Celui-ci est du sucre syntaxique pour:

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-le lorsque vous souhaitez écrire directement sur les éléments du conteneur, par exemple.

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

Celui-ci est du sucre syntaxique pour:

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 tout, tout est "correct" lorsqu'il est utilisé correctement.


2
J'avais l'intention de donner quelques conseils, avec des exemples de codes à compiler (mais étant inefficaces), ou à ne pas compiler, et à expliquer pourquoi, et à essayer de proposer des solutions.
Mr.C64

2
@ Mr.C64 Oh, je suis désolé - je viens de remarquer que c'est une de ces questions de type FAQ. Je suis nouveau sur ce site. Mes excuses! Votre réponse est excellente, je l'ai votée positivement - mais je voulais également fournir une version plus concise pour ceux qui veulent l'essentiel . J'espère que je n'interviens pas.

1
@ Mr.C64 quel est le problème avec OP répondant également à la question? C'est juste une autre réponse valable.
mfontanini

1
@mfontanini: Il n'y a absolument aucun problème si quelqu'un poste une réponse, encore mieux que la mienne. Le but final est de donner une contribution de qualité à la communauté (en particulier pour les débutants qui peuvent se sentir un peu perdus devant les différentes syntaxes et les différentes options qu'offre C ++).
Mr.C64

4

Le bon moyen est toujours

for(auto&& elem : container)

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


6
Mais que se passe-t-il si le conteneur ne renvoie que des références modifiables et que je tiens à préciser que je ne souhaite pas les modifier dans la boucle? Ne devrais-je pas ensuite utiliser auto const &pour clarifier mon intention?
RedX

@RedX: Qu'est-ce qu'une "référence modifiable"?
Courses de légèreté en orbite le

2
@RedX: Les références ne sont jamais constet elles ne sont jamais modifiables. Quoi qu'il en soit, ma réponse est oui, je le ferais .
Courses de légèreté en orbite le

4
Bien que cela puisse fonctionner, je pense que ce sont de mauvais conseils par rapport à l'approche plus nuancée et réfléchie donnée par l'excellente et complète réponse de M. C64 donnée ci-dessus. Réduire au plus petit dénominateur commun n'est pas à quoi sert le C ++.
Jack Aidley

6
Cette proposition d'évolution du langage est en accord avec cette "mauvaise" réponse: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte

1

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 uniquement des conteneurs.

La condition syntaxique pour la boucle for est cette range_expressionprise en charge begin()et en end()tant que fonctions - soit en tant que fonctions membres du type auquel elle est évaluée, soit en tant que fonctions non membres qui prennent une instance du type.

À titre d'exemple artificiel, on peut générer une plage de nombres et parcourir la plage en utilisant 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 la mainfonction suivante ,

#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 la sortie suivante.

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 
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.