156 votes

Boucle d'itération vs boucle d'index

Duplicata possible :
Pourquoi utiliser des itérateurs au lieu d'indices de tableaux ?

Je suis en train de réviser mes connaissances sur le C++ et je suis tombé sur les itérateurs. Une chose que je veux savoir est ce qui les rend si spéciaux et je veux savoir pourquoi cela :

using namespace std;

vector<int> myIntVector;
vector<int>::iterator myIntVectorIterator;

// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

for(myIntVectorIterator = myIntVector.begin(); 
        myIntVectorIterator != myIntVector.end();
        myIntVectorIterator++)
{
    cout<<*myIntVectorIterator<<" ";
    //Should output 1 4 8
}

est meilleur que ça :

using namespace std;

vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

for(int y=0; y<myIntVector.size(); y++)
{
    cout<<myIntVector[y]<<" ";
    //Should output 1 4 8
}

Et oui, je sais que je ne devrais pas utiliser l'espace de noms std. J'ai juste pris cet exemple sur le site de cprogramming. Alors pouvez-vous me dire pourquoi le dernier est pire ? Quelle est la grande différence ?

247voto

TemplateRex Points 26447

La particularité des itérateurs est qu'ils fournissent la colle entre algorithmes et conteneurs . Pour le code générique, il est recommandé d'utiliser une combinaison d'algorithmes STL (par ex. find , sort , remove , copy ) etc. qui effectue le calcul que vous avez en tête sur votre structure de données ( vector , list , map etc.), et de fournir cet algorithme avec des itérateurs dans votre conteneur.

Votre exemple particulier pourrait être écrit comme une combinaison des éléments suivants for_each et l'algorithme vector (voir option 3) ci-dessous), mais ce n'est qu'une des quatre manières distinctes d'itérer sur un std::vector :

1) itération basée sur l'index

for (std::size_t i = 0; i != v.size(); ++i) {
    // access element as v[i]

    // any code including continue, break, return
}

Avantages : familier à toute personne connaissant le code de style C, peut boucler en utilisant différentes strides (par ex. i += 2 ).

Inconvénients : uniquement pour les conteneurs à accès aléatoire séquentiel ( vector , array , deque ), ne fonctionne pas pour list , forward_list ou les conteneurs associatifs. De plus, le contrôle des boucles est un peu verbeux (init, check, increment). Les gens doivent être conscients de l'indexation basée sur 0 en C++.

2) l'itération basée sur l'itérateur

for (auto it = v.begin(); it != v.end(); ++it) {
    // if the current index is needed:
    auto i = std::distance(v.begin(), it); 

    // access element as *it

    // any code including continue, break, return
}

Avantages : plus générique, fonctionne pour tous les conteneurs (même les nouveaux conteneurs associatifs non ordonnés, peut également utiliser des strides différents (par ex. std::advance(it, 2) ) ;

Inconvénients Il faut un travail supplémentaire pour obtenir l'indice de l'élément courant (cela pourrait être O(N) pour une liste ou une forward_list). Encore une fois, le contrôle de la boucle est un peu verbeux (init, check, increment).

3) Algorithme STL for_each + lambda

std::for_each(v.begin(), v.end(), [](T const& elem) {
     // if the current index is needed:
     auto i = &elem - &v[0];

     // cannot continue, break or return out of the loop
});

Avantages Même chose qu'en 2) plus une petite réduction du contrôle de la boucle (pas de vérification et d'incrémentation), cela peut réduire considérablement votre taux d'erreurs (init erroné, vérification ou incrémentation, erreurs de type off-by-one).

Inconvénients : même chose que l'itérateur-boucle explicite, plus des possibilités restreintes pour le contrôle du flux dans la boucle (on ne peut pas utiliser continue, break ou return) et aucune option pour des strides différents (à moins d'utiliser un adaptateur d'itérateur qui surcharge operator++ ).

4) boucle range-for

for (auto& elem: v) {
     // if the current index is needed:
     auto i = &elem - &v[0];

    // any code including continue, break, return
}

Avantages : contrôle de boucle très compact, accès direct à l'élément de courant.

Inconvénients : déclaration supplémentaire pour obtenir l'index. Impossible d'utiliser des strides différents.

Que faut-il utiliser ?

Pour votre exemple particulier d'itération sur std::vector Si vous avez vraiment besoin de l'index (par exemple, pour accéder à l'élément précédent ou suivant, imprimer/enregistrer l'index à l'intérieur de la boucle, etc.) ou si vous avez besoin d'un stride différent de 1, alors je choisirais la boucle explicitement indexée, sinon je choisirais la boucle range-for.

Pour les algorithmes génériques sur des conteneurs génériques, j'opterais pour la boucle d'itérateur explicite, à moins que le code ne contienne aucun contrôle de flux à l'intérieur de la boucle et qu'il ait besoin de stride 1, auquel cas j'opterais pour la STL for_each + un lambda.

14voto

6502 Points 42700

Avec un vecteur, les itérateurs n'offrent pas de réel avantage. La syntaxe est plus moche, plus longue à taper et plus difficile à lire.

L'itération sur un vecteur à l'aide d'itérateurs n'est ni plus rapide ni plus sûre (en fait, si le vecteur est éventuellement redimensionné au cours de l'itération, l'utilisation d'itérateurs vous posera de gros problèmes).

L'idée d'avoir une boucle générique qui fonctionne lorsque vous changerez plus tard le type de conteneur est également absurde dans les cas réels. Malheureusement, le côté sombre d'un langage strictement typé sans inférence de typage sérieuse (un peu mieux maintenant avec C++11, cependant) est que vous devez dire quel est le type de tout à chaque étape. Si vous changez d'avis plus tard, vous devrez toujours faire le tour et tout changer. De plus, les différents conteneurs ont des compromis très différents et changer de type de conteneur n'est pas quelque chose qui arrive souvent.

Le seul cas dans lequel l'itération doit être conservée si possible de manière générique est celui de l'écriture de code template, mais ce n'est pas le cas le plus fréquent (je l'espère pour vous).

Le seul problème présent dans votre boucle d'index explicite est que size renvoie une valeur non signée (un bug de conception du C++) et la comparaison entre signé et non signé est dangereuse et surprenante, il vaut donc mieux l'éviter. Si vous utilisez un compilateur décent avec les avertissements activés, il devrait y avoir un diagnostic à ce sujet.

Notez que la solution n'est pas d'utiliser un non signé comme index, parce que l'arithmétique entre des valeurs non signées est aussi apparemment illogique (c'est de l'arithmétique modulo, et x-1 peut être plus grande que x ). Vous devriez plutôt convertir la taille en un nombre entier avant de l'utiliser. Il s'agit de mai Il est utile d'utiliser des tailles et des index non signés (en faisant très attention à chaque expression que vous écrivez) uniquement si vous travaillez sur une implémentation C++ 16 bits ( 16 bits était la raison d'avoir des valeurs non signées dans les tailles ).

Comme une erreur typique que la taille non signée peut introduire considérer :

void drawPolyline(const std::vector<P2d>& points)
{
    for (int i=0; i<points.size()-1; i++)
        drawLine(points[i], points[i+1]);
}

Ici, le bogue est présent car si vous passez un fichier vide points vecteur la valeur points.size()-1 sera un nombre positif énorme, ce qui vous fera boucler dans un segfault. Une solution efficace pourrait être

for (int i=1; i<points.size(); i++)
    drawLine(points[i - 1], points[i]);

mais je préfère personnellement toujours supprimer unsinged -avec int(v.size()) .

PS : Si vous ne voulez vraiment pas réfléchir par vous-même aux implications et que vous voulez simplement qu'un expert vous le dise, considérez qu'un certain nombre d'experts C++ reconnus dans le monde entier sont d'accord et ont exprimé des opinions à ce sujet. Les valeurs non signées sont une mauvaise idée, sauf pour les manipulations de bits. .

La découverte de la laideur de l'utilisation des itérateurs dans le cas de l'itération jusqu'à l'avant-dernier est laissée comme un exercice pour le lecteur.

9voto

Alok Save Points 115848

Les itérateurs rendent votre code plus générique.
Chaque conteneur de la bibliothèque standard fournit un itérateur, donc si vous changez votre classe de conteneur à l'avenir, la boucle ne sera pas affectée.

7voto

billz Points 28166

Les itérateurs sont de premier choix par rapport aux operator[] . C++11 fournit std::begin() , std::end() fonctions.

Comme votre code n'utilise que std::vector Cependant, je ne peux pas dire qu'il y ait une grande différence entre les deux codes, operator [] peut ne pas fonctionner comme vous le souhaitez. Par exemple, si vous utilisez map, operator[] insérera un élément s'il n'est pas trouvé.

De plus, en utilisant iterator votre code devient plus portable entre les conteneurs. Vous pouvez changer de conteneur à partir de std::vector a std::list ou autre conteneur librement sans changer grand chose si vous utilisez un itérateur ; cette règle ne s'applique pas à operator[] .

4voto

Mark Garcia Points 9851

Cela dépend toujours de ce dont vous avez besoin.

Vous devez utiliser operator[] quand vous besoin de l'accès direct aux éléments du vecteur (lorsque vous avez besoin d'indexer un élément spécifique du vecteur). Il n'y a rien de mal à l'utiliser par rapport aux itérateurs. Cependant, vous devez décider par vous-même quelle ( operator[] ou itérateurs) convient le mieux à vos besoins.

L'utilisation d'itérateurs vous permettrait de passer à d'autres types de conteneurs sans trop modifier votre code. En d'autres termes, l'utilisation d'itérateurs rendrait votre code plus générique et ne dépendrait pas d'un type de conteneur particulier.

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