Considérons une fonction de modèle écrite en C++ 11 qui itère sur un conteneur . Veuillez exclure de la considération la syntaxe de la boucle de plage car elle n’est pas encore prise en charge par le compilateur avec lequel je travaille.
template <typename Container>
void DoSomething(const Container& i_container)
{
// Option #1
for (auto it = std::begin(i_container); it != std::end(i_container); ++it)
{
// do something with *it
}
// Option #2
std::for_each(std::begin(i_container), std::end(i_container),
[] (typename Container::const_reference element)
{
// do something with element
});
}
Quels sont les avantages/inconvénients de for loop vs std::for_each
en termes de:
une performance? (Je ne m'attends à aucune différence)
b) lisibilité et maintenabilité?
Ici, je vois de nombreux inconvénients de for_each
. Il n'accepterait pas un tableau de style c tant que la boucle l'accepterait. La déclaration du paramètre formel lambda est très détaillée, il est donc impossible d’utiliser auto
. Il n'est pas possible de sortir de for_each
.
Avant 11 jours C++, les arguments contre for
nécessitaient de spécifier le type de l'itérateur (ne tient plus) et une possibilité facile de ne pas taper correctement la condition de boucle (je n'ai jamais commis une telle erreur depuis 10 ans). .
En conclusion, mes pensées sur for_each
contredisent l’opinion commune. Qu'est-ce que j'oublie ici?
Je pense qu'il y a d'autres différences non encore couvertes par les réponses jusqu'à présent.
un for_each
peut accepter tout objet appelable approprié, permettant ainsi de "recycler" le corps de la boucle pour différentes boucles for. Par exemple (pseudo-code)
for( range_1 ) { lengthy_loop_body } // many lines of code
for( range_2 ) { lengthy_loop_body } // the same many lines of code again
devient
auto loop_body = some_lambda; // many lines of code here only
std::for_each( range_1 , loop_body ); // a single line of code
std::for_each( range_2 , loop_body ); // another single line of code
évitant ainsi les duplications et simplifiant la maintenance du code. (Bien entendu, dans un drôle de mélange de styles, on pourrait également utiliser une approche similaire avec la boucle for
.)
une autre différence concerne la rupture de la boucle (avec break
ou return
dans la boucle for
). Autant que je sache, dans une boucle for_each
, cela ne peut être fait qu’en levant une exception. Par exemple
for( range )
{
some code;
if(condition_1) return x; // or break
more code;
if(condition_2) continue;
yet more code;
}
devient
try {
std::for_each( range , [] (const_reference x)
{
some code;
if(condition_1) throw x;
more code;
if(condition_2) return;
yet more code;
} );
} catch(const_reference r) { return r; }
avec les mêmes effets concernant l’appel de destructeurs pour les objets ayant la portée du corps de la boucle et du corps de la fonction (autour de la boucle).
le principal avantage de for_each
est, à mon humble avis, que l’on peut le surcharger pour certains types de conteneurs, lorsque l’itération simple n’est pas aussi efficace. Par exemple, considérons un conteneur contenant une liste chaînée de blocs de données, chaque bloc contenant un tableau d'éléments contigus, similaire à (en omettant le code non pertinent).
namespace my {
template<typename data_type, unsigned block_size>
struct Container
{
struct block
{
const block*NEXT;
data_type DATA[block_size];
block() : NEXT(0) {}
} *HEAD;
};
}
alors, un itérateur avant approprié pour ce type devrait vérifier la fin du bloc à chaque incrément et l'opérateur de comparaison doit comparer le pointeur de bloc et l'index dans chaque bloc (en omettant le code non pertinent):
namespace my {
template<typename data_type, unsigned block_size>
struct Container
{
struct iterator
{
const block*B;
unsigned I;
iterator() = default;
iterator&operator=(iterator const&) = default;
iterator(const block*b, unsigned i) : B(b), I(i) {}
iterator& operator++()
{
if(++I==block_size) { B=B->NEXT; I=0; } // one comparison and branch
return*this;
}
bool operator==(const iterator&i) const
{ return B==i.B && I==i.I; } // one or two comparisons
bool operator!=(const iterator&i) const
{ return B!=i.B || I!=i.I; } // one or two comparisons
const data_type& operator*() const
{ return B->DATA[I]; }
};
iterator begin() const
{ return iterator(HEAD,0); }
iterator end() const
{ return iterator(0,0); }
};
}
ce type d'itérateur fonctionne correctement avec for
et for_each
, par exemple
my::Container<int,5> C;
for(auto i=C.begin();
i!=C.end(); // one or two comparisons here
++i) // one comparison here and a branch
f(*i);
mais nécessite deux à trois comparaisons par itération ainsi qu’une branche. Une méthode plus efficace consiste à surcharger la fonction for_each()
pour boucler le pointeur du bloc et l'index séparément:
namespace my {
template<typename data_type, int block_size, typename FuncOfDataType>
FuncOfDataType&&
for_each(typename my::Container<data_type,block_size>::iterator i,
typename my::Container<data_type,block_size>::iterator const&e,
FuncOfDataType f)
{
for(; i.B != e.B; i.B++,i.I=0)
for(; i.I != block_size; i.I++)
f(*i);
for(; i.I != e.I; i.I++)
f(*i);
return std::move(f);
}
}
using my::for_each; // ensures that the appropriate
using std::for_each; // version of for_each() is used
qui ne nécessite qu'une seule comparaison pour la plupart des itérations et ne comporte aucune branche (notez que les branches peuvent avoir un impact désastreux sur les performances). Notez que nous n'avons pas besoin de définir cela dans l'espace de noms std
(ce qui peut être illégal), mais nous pouvons nous assurer que la version correcte est utilisée par les directives using
appropriées. Cela équivaut à using std::swap;
lors de la spécialisation de swap()
pour certains types définis par l'utilisateur.
En ce qui concerne les performances, votre boucle for
appelle std::end
à plusieurs reprises, contrairement à std::for_each
. Cela peut entraîner ou non une différence de performance en fonction du conteneur utilisé.
La version std::for_each
visitera chaque élément exactement une fois. Une personne qui lit le code peut le savoir dès qu’ils voient std::for_each
, car rien ne peut être fait dans le lambda pour déranger l’itérateur. Dans la boucle for traditionnelle, vous devez étudier le corps de la boucle pour rechercher un flux de contrôle inhabituel (continue
, break
, return
) et une liaison avec l'itérateur (par exemple, dans ce cas, ignorez l'élément suivant avec ++it
).
Vous pouvez modifier trivialement l'algorithme dans la solution lambda. Par exemple, vous pouvez créer un algorithme qui visite chaque nième élément. Dans de nombreux cas, vous ne vouliez pas vraiment une boucle for, mais un algorithme différent comme copy_if
. En utilisant un algorithme + lambda, il est souvent plus facile de changer et est un peu plus concis.
D'un autre côté, les programmeurs sont beaucoup plus habitués aux boucles traditionnelles et peuvent donc trouver plus difficile de lire algorithme + lambda.
Premièrement, je ne vois pas beaucoup de différence entre ces deux types, car for_each est implémenté à l'aide de for loop. Mais notez que for_each est une fonction qui a une valeur de retour.
Deuxièmement, je vais utiliser la syntaxe Range Loop une fois disponible dans ce cas, car ce jour viendrait de toute façon bientôt.
Effectivement; dans le cas d'utilisation d'une expression Lambda, vous devez déclarer le type et le nom du paramètre afin que rien ne soit gagné.
Mais ce sera génial dès que vous voudrez appeler une fonction (nommée) ou un objet fonction avec cela. (N'oubliez pas que vous pouvez combiner des éléments fonctionnels via std::bind
.)
Les livres de Scott Meyers (je crois que c’était Effective STL ) décrivent de tels styles de programmation très bien et clairement.