Vous préférez les algorithmes aux boucles écrites à la main?

10

Lequel des énoncés suivants vous semble le plus lisible? La boucle manuscrite:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Ou l'invocation de l'algorithme:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Je me demande si std::for_eachcela en vaut vraiment la peine, étant donné qu'un exemple aussi simple nécessite déjà beaucoup de code.

Quelles sont vos réflexions à ce sujet?

fredoverflow
la source
2
Pour C ++, la réponse est assez évidente. Mais dans d'autres langages, les deux sont à peu près égaux (par exemple Python:, map(bar.process, vec)bien que la carte des effets secondaires soit déconseillée et que les compréhensions de liste / expressions de générateur soient recommandées par rapport à la carte).
5
Et puis il y a aussi BOOST_FOREACH...
Benjamin Bannier le
Donc, l'article dans la STL efficace ne vous a pas convaincu? stackoverflow.com/questions/135129/…
Job

Réponses:

18

Il y a une raison pour laquelle les lambdas ont été introduits, et c'est parce que même le comité standard reconnaît que la deuxième forme est nulle. Utilisez le premier formulaire jusqu'à ce que vous obteniez le support C ++ 0x et lambda.

DeadMG
la source
2
Le même raisonnement peut être utilisé pour justifier la deuxième forme: le Comité Standard a reconnu qu'il était tellement supérieur qu'il a introduit des lambdas pour le rendre encore meilleur. :-p (+1 néanmoins)
Konrad Rudolph
Je ne pense pas que le deuxième soit si mauvais. Mais ça peut être mieux. Par exemple, vous avez délibérément rendu l'objet Bar plus difficile à utiliser en n'ajoutant pas operator (). En l'utilisant, cela simplifie considérablement le point d'appel dans la boucle et rend le code bien rangé.
Martin York
Remarque: le support lambda a été ajouté en raison de deux plaintes majeures. 1) Le passe-partout standard devait transmettre les paramètres aux foncteurs. 2) Ne pas avoir le code au point d'appel. Votre code ne satisfait à aucun de ces éléments car vous appelez simplement une méthode. Donc 1) pas de code supplémentaire pour passer les paramètres 2) Le code n'est pas déjà au point d'appel, donc lambda n'améliorera pas la maintenabilité du code. Alors oui, lambda est un excellent ajout. Ce n'est pas un bon argument pour eux.
Martin York
9

Utilisez toujours la variante qui décrit le mieux ce que vous avez l'intention de faire. C'est

Pour chaque élément xde vec, faire bar.process(x).

Examinons maintenant les exemples:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Nous en avons for_eachaussi un - yippeh . Nous avons la [begin; end)gamme sur laquelle nous voulons opérer.

En principe, l'algorithme était beaucoup plus explicite et donc préférable à toute implémentation manuscrite. Mais alors ... des reliures? Memfun? Fondamentalement, C ++ interna de la façon d'obtenir une fonction membre? Pour ma tâche, je m'en fous ! Je ne veux pas non plus souffrir de cette syntaxe bavarde et effrayante.

Maintenant, l'autre possibilité:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Certes, c'est un modèle courant à reconnaître, mais ... création d'itérateurs, bouclage, incrémentation, déréférencement. Ce sont aussi des choses dont je ne me soucie pas pour accomplir ma tâche.

Certes, il semble waay mieux que la première solution (au moins, la boucle corps est flexible et tout à fait explicite), mais encore, ce n'est pas vraiment que grand. Nous utiliserons celui-ci si nous n'avions pas de meilleure possibilité, mais peut-être que nous avons ...

Une meilleure façon?

Revenons maintenant à for_each. Ne serait-il pas formidable de dire littéralement for_each et d' être flexible dans l'opération qui doit être faite aussi? Heureusement, depuis les lambdas C ++ 0x, nous sommes

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Maintenant que nous avons trouvé une solution abstraite et générique à de nombreuses situations connexes, il convient de noter que dans ce cas particulier, il existe un favori absolu # 1 :

foreach(const Foo& x, vec) bar.process(x);

Cela ne peut vraiment pas être beaucoup plus clair que cela. Heureusement, C ++ 0x get est une syntaxe similaire intégrée !

Dario
la source
2
Ledit "syntaxe similaire" étant for (const Foo& x : vec) bar.process(x);, ou utilisant const auto&si vous le souhaitez.
Jon Purdy
const auto&est possible? Je ne savais pas que - grande info!
Dario
8

Parce que c'est tellement illisible?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}
Martin Beckett
la source
4
Désolé, mais pour une raison quelconque, il est dit "censuré" où je pense que votre code devrait être.
Mateen Ulhaq
6
Votre code donne un avertissement au compilateur, car comparer un intavec un size_test dangereux. De plus, l'indexation ne fonctionne pas avec tous les conteneurs, par exemple std::set.
fredoverflow du
2
Ce code ne fonctionnera que pour les conteneurs avec un opérateur d'indexation, dont il existe peu. Il lie l'algorithme indûment - et si une carte <> convenait mieux? Les boucles for et for_each d'origine ne seraient pas affectées.
JBRWilkinson
2
Et combien de fois concevez-vous du code pour un vecteur et décidez-vous ensuite de l'échanger contre une carte? Comme si la conception était la priorité des langues.
Martin Beckett
2
L'itération des collections par index est déconseillée car certaines collections ont de mauvaises performances d'indexation, tandis que toutes les collections se comportent bien lors de l'utilisation d'un itérateur.
slaphappy
3

en général, la première forme est lisible par quasiment tous ceux qui savent ce qu'est une boucle, quel que soit leur arrière-plan.

également en général, le second n'est pas du tout lisible: assez facile à comprendre ce que for_each fait, mais si vous ne l'avez jamais vu, std::bind1st(std::mem_fun_refje peux imaginer que c'est difficile à comprendre.

stijn
la source
2

En fait, même avec C ++ 0x, je ne suis pas sûr d' for_eachobtenir beaucoup d'amour.

for(Foo& foo: vec) { bar.process(foo); }

Est bien plus lisible.

La seule chose que je n'aime pas dans les algorithmes (en C ++), c'est qu'ils raisonnent sur les itérateurs, ce qui rend les déclarations très verbeuses.

Matthieu M.
la source
2

Si vous aviez écrit bar comme foncteur, ce serait beaucoup plus simple:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Ici, le code est assez lisible.

Martin York
la source
2
C'est assez lisible à part le fait que vous venez d'écrire un foncteur.
Winston Ewert
Voir ici pourquoi je pense que ce code est pire.
sbi
1

Je préfère ce dernier car il est net et propre. Il fait en fait partie de nombreux autres langages mais en C ++, il fait partie de la bibliothèque. Ce n'est pas vraiment important.

Sarat
la source
0

Maintenant, ce problème n'est pas spécifique au C ++, je dirais.

Le fait est que le passage d'un foncteur dans une autre fonction n'est pas censé remplacer les boucles .
Il est censé simplifier certains modèles, qui deviennent très naturels avec la programmation fonctionnelle, comme le visiteur et l' observateur , pour n'en nommer que deux, qui me viennent immédiatement à l'esprit.
Dans les langages qui manquent de fonctions de premier ordre (Java est probablement le meilleur exemple), de telles approches nécessitent toujours l'implémentation d'une interface donnée, qui est assez verbeuse et redondante.

Une utilisation courante que je vois beaucoup dans d'autres langues serait:

someCollection.forEach(someUnaryFunctor);

L'avantage de ceci est que vous n'avez pas besoin de savoir comment someCollectionest réellement implémenté ou quoi someUnaryFunctor. Tout ce que vous devez savoir, c'est que sa forEachméthode itérera tous les éléments de la collection, en les passant à la fonction donnée.

Personnellement, si vous êtes en mesure de disposer de toutes les informations sur la structure de données que vous souhaitez parcourir et toutes les informations sur ce que vous voulez faire à chaque étape de l'itération, une approche fonctionnelle consiste à trop compliquer les choses, en particulier dans une langue, où cela est apparemment assez fastidieux.

En outre, vous devez garder à l'esprit que l'approche fonctionnelle est plus lente, car vous avez beaucoup d'appels, qui ont un certain coût, que vous n'avez pas dans une boucle for.

back2dos
la source
1
"En outre, vous devez garder à l'esprit que l'approche fonctionnelle est plus lente, car vous avez beaucoup d'appels, qui ont un certain coût, que vous n'avez pas dans une boucle for." ? Cette déclaration n'est pas prise en charge. L'approche fonctionnelle n'est pas forcément plus lente, d'autant plus qu'il peut y avoir des optimisations sous le capot. Et même si vous comprenez parfaitement votre boucle, elle sera probablement plus propre sous sa forme plus abstraite.
Andres F.
@Andres F: Donc ça a l'air plus propre? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));Et c'est plus lent au moment de l'exécution ou de la compilation (si vous avez de la chance). Je ne vois pas pourquoi le remplacement des structures de contrôle de flux par des appels de fonction améliore le code. À propos de toute alternative présentée ici est plus lisible.
back2dos
0

Je pense que le problème ici est que ce for_eachn'est pas vraiment un algorithme, c'est juste une manière différente (et généralement inférieure) d'écrire une boucle écrite à la main. de ce point de vue, il devrait être l'algorithme standard le moins utilisé, et oui, vous pourriez aussi bien utiliser une boucle for. cependant, les conseils dans le titre sont toujours valables car il existe plusieurs algorithmes standard adaptés à des utilisations plus spécifiques.

Lorsqu'il existe un algorithme qui fait plus étroitement ce que vous voulez, alors oui, vous devriez préférer les algorithmes aux boucles écrites à la main. des exemples évidents ici sont le tri avec sortou stable_sortou la recherche avec lower_bound, upper_boundou equal_range, mais la plupart d'entre eux ont une situation dans laquelle ils sont préférables à utiliser sur une boucle codée à la main.

jk.
la source
0

Pour ce petit extrait de code, les deux sont également lisibles pour moi. En général, je trouve les boucles manuelles sujettes aux erreurs et préfère utiliser des algorithmes, mais cela dépend vraiment d'une situation concrète.

Nemanja Trifunovic
la source