En lisant diverses questions ici sur Stack Overflow sur les itérateurs C++ et les performances **, j'ai commencé à me demander si for(auto& elem : container)
était "développé" par le compilateur dans la meilleure version possible? (Un peu comme auto
, que le compilateur infère immédiatement dans le bon type et n'est donc jamais plus lent et parfois plus rapide).
** Par exemple, est-ce important si vous écrivez
for(iterator it = container.begin(), eit = container.end(); it != eit; ++it)
ou
for(iterator it = container.begin(); it != container.end(); ++it)
pour les conteneurs non invalidants?
Le Standard est votre ami, voir [stmt.ranged]/1
Pour une déclaration basée sur une plage pour le formulaire
for ( for-range-declaration : expression ) statement
que range-init soit équivalent à l'expression entourée de parenthèses
( expression )
et pour une déclaration basée sur la plage pour la déclaration de la forme
for ( for-range-declaration : braced-init-list ) statement
laissez range-init être équivalent à la braced-init-list. Dans chaque cas, une instruction
for
basée sur une plage équivaut à{ auto && __range = range-init; for ( auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin ) { for-range-declaration = *__begin; statement } }
Alors oui, la Norme garantit que la meilleure forme possible est atteinte.
Et pour un certain nombre de conteneurs, tels que vector
, c'est un comportement indéfini de les modifier (insérer/effacer) pendant cette itération.
Range-for est aussi rapide que possible car il met en cache l'itérateur final[ citation fournie ], utilise la pré-incrémentation et ne déréférence l'itérateur qu'une seule fois.
donc si vous avez tendance à écrire:
for(iterator i = cont.begin(); i != cont.end(); i++) { /**/ }
Ensuite, oui, range-for peut être légèrement plus rapide, car il est également plus facile d'écrire, il n'y a aucune raison de ne pas l'utiliser (le cas échéant).
N.B. J'ai dit que c'est aussi rapide que possible, ce n'est cependant pas plus rapide que possible. Vous pouvez obtenir exactement les mêmes performances si vous écrivez soigneusement vos boucles manuelles.
Par curiosité, j'ai décidé de regarder le code Assembly pour les deux approches:
int foo1(const std::vector<int>& v) {
int res = 0;
for (auto x : v)
res += x;
return res;
}
int foo2(const std::vector<int>& v) {
int res = 0;
for (std::vector<int>::const_iterator it = v.begin(); it != v.end(); ++it)
res += *it;
return res;
}
Et le code d'assemblage (avec -O3 et gcc 4.6) est exactement le même pour les deux approches (code pour foo2
est omis, car il est exactement le même):
080486d4 <foo1(std::vector<int, std::allocator<int> > const&)>:
80486d4: 8b 44 24 04 mov 0x4(%esp),%eax
80486d8: 8b 10 mov (%eax),%edx
80486da: 8b 48 04 mov 0x4(%eax),%ecx
80486dd: b8 00 00 00 00 mov $0x0,%eax
80486e2: 39 ca cmp %ecx,%edx
80486e4: 74 09 je 80486ef <foo1(std::vector<int, std::allocator<int> > const&)+0x1b>
80486e6: 03 02 add (%edx),%eax
80486e8: 83 c2 04 add $0x4,%edx
80486eb: 39 d1 cmp %edx,%ecx
80486ed: 75 f7 jne 80486e6 <foo1(std::vector<int, std::allocator<int> > const&)+0x12>
80486ef: f3 c3 repz ret
Donc, oui, les deux approches sont les mêmes.
UPDATE : La même observation s'applique aux autres conteneurs (ou types d'éléments) tels que vector<string>
et map<string, string>
. Dans ces cas, il est particulièrement important d'utiliser une référence dans la boucle à distance. Sinon, un temporaire est créé et beaucoup de code supplémentaire apparaît (dans les exemples précédents, il n'était pas nécessaire car les vector
ne contenaient que des valeurs de int
).
Pour le cas de map<string, string>
l'extrait de code C++ utilisé est:
int foo1(const std::map<std::string, std::string>& v) {
int res = 0;
for (const auto& x : v) {
res += (x.first.size() + x.second.size());
}
return res;
}
int foo2(const std::map<std::string, std::string>& v) {
int res = 0;
for (auto it = v.begin(), end = v.end(); it != end; ++it) {
res += (it->first.size() + it->second.size());
}
return res;
}
Et le code d'assemblage (dans les deux cas) est:
8048d70: 56 Push %esi
8048d71: 53 Push %ebx
8048d72: 31 db xor %ebx,%ebx
8048d74: 83 ec 14 sub $0x14,%esp
8048d77: 8b 74 24 20 mov 0x20(%esp),%esi
8048d7b: 8b 46 0c mov 0xc(%esi),%eax
8048d7e: 83 c6 04 add $0x4,%esi
8048d81: 39 f0 cmp %esi,%eax
8048d83: 74 1b je 8048da0
8048d85: 8d 76 00 lea 0x0(%esi),%esi
8048d88: 8b 50 10 mov 0x10(%eax),%edx
8048d8b: 03 5a f4 add -0xc(%edx),%ebx
8048d8e: 8b 50 14 mov 0x14(%eax),%edx
8048d91: 03 5a f4 add -0xc(%edx),%ebx
8048d94: 89 04 24 mov %eax,(%esp)
8048d97: e8 f4 fb ff ff call 8048990 <std::_Rb_tree_increment(std::_Rb_tree_node_base const*)@plt>
8048d9c: 39 c6 cmp %eax,%esi
8048d9e: 75 e8 jne 8048d88
8048da0: 83 c4 14 add $0x14,%esp
8048da3: 89 d8 mov %ebx,%eax
8048da5: 5b pop %ebx
8048da6: 5e pop %esi
8048da7: c3 ret
C'est peut-être plus rapide, dans de rares cas. Comme vous ne pouvez pas nommer l'itérateur, un optimiseur peut plus facilement prouver que votre boucle ne peut pas modifier l'itérateur. Cela affecte par exemple optimisations de déroulement de boucle.
Non. C'est la même chose que l'ancienne boucle for
avec les itérateurs. Après tout, le for
basé sur la plage fonctionne avec les itérateurs en interne. Le compilateur produit juste du code équivalent pour les deux.