Duplicate possible:
Pourquoi utiliser des itérateurs plutôt que des index de tableaux?
Je passe en revue mes connaissances sur C++ et je suis tombé sur des itérateurs. Une chose que je veux savoir est ce qui les rend si spécial et je veux savoir pourquoi ceci:
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
}
c'est mieux 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. Je viens de prendre cet exemple sur le site Web cprogramming. Alors pouvez-vous s'il vous plaît me dire pourquoi ce dernier est pire? Quelle est la grande différence?
La particularité des itérateurs est qu'ils fournissent le lien entre algorithmes et conteneurs . Pour le code générique, il est recommandé d’utiliser une combinaison d’algorithmes STL (par exemple, find
, sort
, remove
, copy
), etc., qui effectue le calcul que vous tenez compte de votre structure de données (vector
, list
, map
etc.) et fournissez cet algorithme avec des itérateurs dans votre conteneur.
Votre exemple particulier pourrait être écrit comme une combinaison de l'algorithme for_each
et du conteneur 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 un 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 familiarisée avec le code de style C, peut effectuer une boucle à l'aide de différentes foulées (par exemple i += 2
).
Inconvénients : uniquement pour les conteneurs à accès aléatoire séquentiels (vector
, array
, deque
), ne fonctionne pas pour list
, forward_list
ou les conteneurs associatifs. De plus, le contrôle de boucle est un peu prolixe (init, check, increment). Les utilisateurs doivent être conscients de l'indexation basée sur 0 en C++.
2) itération basée sur un 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 différentes étapes (par exemple, std::advance(it, 2)
);
Inconvénients : nécessite un travail supplémentaire pour obtenir l'index de l'élément en cours (par exemple, O(N) pour list ou forward_list). Encore une fois, le contrôle de la boucle est un peu prolixe (init, check, increment).
3) STL pour l'algorithme + 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 : identiques à 2) plus une petite réduction du contrôle de boucle (pas de contrôle ni d’incrément), ceci peut considérablement réduire votre taux de bogues (mauvaise init, vérification ou incrémentation, off-by- une erreur).
Inconvénients : identiques à la boucle d'itérateur explicite plus les possibilités limitées de contrôle de flux dans la boucle (impossible d'utiliser les méthodes continue, pause ou retour) et aucune option pour des pas différents (sauf si vous utilisez un itérateur). adaptateur qui surcharge operator++
).
4) boucle pour plage
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 en cours.
Inconvénients : instruction supplémentaire pour obtenir l'index. Impossible d'utiliser différentes foulées.
Que faut-il utiliser?
Pour votre exemple particulier d'itération sur std::vector
: si vous avez vraiment besoin de l'index (par exemple, accéder à l'élément précédent ou suivant, imprimer/consigner l'index à l'intérieur de la boucle, etc.) ou si vous avez besoin d'une foulée différente de 1, irait pour la boucle explicitement indexée, sinon j'irais pour la boucle plage-pour.
Pour les algorithmes génériques sur les conteneurs génériques, je choisirais la boucle d'itérateur explicite, à moins que le code ne contienne aucun contrôle de flux à l'intérieur de la boucle et nécessite l'étape 1; dans ce cas, je choisirais le STL for_each
+ a lambda.
Les itérateurs rendent votre code plus générique.
Chaque conteneur de bibliothèque standard fournit un itérateur. Par conséquent, si vous modifiez votre classe de conteneur à l'avenir, la boucle ne sera pas affectée.
Les itérateurs sont le premier choix sur operator[]
. C++ 11 fournit les fonctions std::begin()
, std::end()
.
Étant donné que votre code utilise uniquement std::vector
, je ne peux pas dire qu'il existe une grande différence entre les deux codes. Toutefois, operator []
risque de 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 de std::vector
à std::list
ou à un autre conteneur librement sans trop changer, si vous utilisez itérateur, une telle règle ne s'applique pas à operator[]
.
Avec un vecteur, les itérateurs n'offrent aucun avantage réel. La syntaxe est plus laide, plus longue à taper et plus difficile à lire.
Itérer sur un vecteur à l'aide d'itérateurs n'est pas plus rapide ni plus sûr (en fait, si le vecteur est éventuellement redimensionné pendant l'itération, utiliser des itérateurs vous mettra dans de gros problèmes).
L'idée de disposer d'une boucle générique qui fonctionne lorsque vous modifiez ultérieurement le type de conteneur est également généralement absurde dans des cas réels. Malheureusement, le côté obscur d'un langage strictement typé sans inférence sérieuse (un peu mieux maintenant avec C++ 11, cependant) est qu'il est nécessaire de dire quel est le type de tout à chaque étape. Si vous changez d'avis plus tard, il vous faudra tout de même tout changer. De plus, les différents conteneurs ont des compromis très différents et le changement de type de conteneur n’est pas fréquent.
Le seul cas dans lequel une itération devrait être conservée si possible générique est lors de l'écriture de code de modèle, mais ce n'est pas le cas le plus fréquent (j'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 bogue de conception de C++) et que la comparaison entre signé et non signé est dangereuse et surprenante, mieux vaut donc l'éviter. Si vous utilisez un compilateur décent avec des avertissements activés, un diagnostic devrait en être fait.
Notez que la solution consiste à ne pas utiliser d'index non-indexé comme index, car l'arithmétique entre les valeurs non signées est également apparemment illogique (arithmétique modulo, et x-1
peut être plus grand que x
). Vous devriez plutôt convertir la taille en un entier avant de l’utiliser. Il peut avoir un sens d'utiliser des tailles et des index non signés (en portant beaucoup d'attention à chaque expression que vous écrivez) uniquement si vous travaillez sur un 16 bit implémentation C++ ( 16 bits était la raison d'avoir des valeurs non signées dans les tailles ).
En tant qu'erreur typique que la taille non signée peut introduire, prenez en compte:
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 parce que si vous passez un vecteur vide points
, la valeur points.size()-1
sera un énorme nombre positif, ce qui vous rendra en boucle dans une erreur de segmentation. Une solution de travail pourrait être
for (int i=1; i<points.size(); i++)
drawLine(points[i - 1], points[i]);
mais personnellement, je préfère toujours supprimer unsinged
- ness avec int(v.size())
.
La laideur d'utiliser des itérateurs dans ce cas est laissée comme un exercice pour le lecteur.
Cela dépend toujours de ce dont vous avez besoin.
Vous devez utiliser operator[]
lorsque vous besoin accès direct aux éléments du vecteur (lorsque vous devez indexer un élément spécifique du vecteur). Il n'y a rien de mal à l'utiliser sur des itérateurs. Cependant, vous devez décider vous-même lequel (operator[]
ou itérateurs) convient le mieux à vos besoins.
L'utilisation d'itérateurs vous permettrait de basculer vers 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épendait pas d'un type de conteneur particulier.
En écrivant votre code client en termes d'itérateurs, vous supprimez complètement le conteneur.
Considérons ce code:
class ExpressionParser // some generic arbitrary expression parser
{
public:
template<typename It>
void parse(It begin, const It end)
{
using namespace std;
using namespace std::placeholders;
for_each(begin, end,
bind(&ExpressionParser::process_next, this, _1);
}
// process next char in a stream (defined elsewhere)
void process_next(char c);
};
code client:
ExpressionParser p;
std::string expression("SUM(A) FOR A in [1, 2, 3, 4]");
p.parse(expression.begin(), expression.end());
std::istringstream file("expression.txt");
p.parse(std::istringstream<char>(file), std::istringstream<char>());
char expr[] = "[12a^2 + 13a - 5] with a=108";
p.parse(std::begin(expr), std::end(expr));
Edit: Considérons votre exemple de code original, implémenté avec:
using namespace std;
vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.Push_back(1);
myIntVector.Push_back(4);
myIntVector.Push_back(8);
copy(myIntVector.begin(), myIntVector.end(),
std::ostream_iterator<int>(cout, " "));
Ce qui est bien avec l’itérateur, c’est que plus tard, si vous voulez passer votre vecteur à un autre conteneur STD. Ensuite, le forloop fonctionnera toujours.