:king:
:lahola:
:chin:
Version imprimable
:king:
:lahola:
:chin:
Ca part de la constatation qui me semble être peu contestable que le cerveau du programmeur n'a pas une puissance de calcul infinie, et que la sémantique (le sens) d'un ptit foreach est beaucoup plus directement limpide que celui d'une boucle de 0 à la taille du tableau moins un et de l'affectation du contenu correspondant du tableau dans une variable locale. Ca permet d'exprimer dans le code le sens de ce que l'on veut faire sans avoir le nez dans le guidon à se concentrer exclusivement sur "comment" on veut le faire. Dit autrement, ça permet de prendre un peu de hauteur, ça rend le code plus clair, concis et lisible. Le programmeur y gagne parce qu'il a moins à écrire et à plus confiance dans son code. Les relecteurs éventuels y gagnent parce que le code est plus clair. Qui y perd quoi que ce soit ?
En quoi une boucle foreach introduit des bugs qu'il n'y aurait pas dans une boucle for ???
En quoi
fait une "inflation de la complexité des logiciel" par rapport àCode:
1
2
3 int sum = 0; foreach(e : t) sum += e;
??Code:
1
2
3
4
5 int sum = 0; int n = t.size(); for(int i = 0; i < n; ++i) sum += t[i];
"Vu que je pré-alloue tout, j'ai jamais de problème avec l'allocation".
Super...
Elle est où la boite noire ?? C'est un foreach. Le sens en est parfaitement clair !
Et puis pourquoi tu utilises des boucles for, et tu ne te limites pas uniquement à des boucle while ? C'est vachement plus mieux quand même ! Au moins on ne délègue pas...
Tu n'oublieras pas juste avant la boucle le commentaire "// On parcourt tous les éléments du tableau."
On ne sait jamais, hein, d'ici à ce que l'on tombe sur des développeurs qui ne savent pas non plus lire du code...
Il n'y a pas de problèmes de "compréhension" d'une boucle "for" par rapport à "foreach", c'est plutôt un problème de parcours parfois complexe (certaines collections / maps par exemple) et de feignantise et/ou d'ignorance des limites des éléments parcourus.
Si un développeur n'arrive pas à comprendre une simple boucle "for" et/ou mets des commentaires qui reprennent en français le code ("on affecte X à Y", "on retourne la valeur", etc.), faut plutôt s'inquiéter de lui laisser toucher du code critique, que ce soit au niveau performances ou au niveau sûreté... Cf. titre du sujet, pour rappel.
Déjà eu le cas sur des tableaux associatifs, les éléments non vectorisés étaient parcourus eux aussi en plus de la partie vecteur...
En l'occurrence, il y a manifestement un problème de traduction du "foreach" au niveau performances (peut-être corrigé depuis, je ne suis pas spécialement ça non plus). Il peut y avoir des effets de bord. Et un développeur est censé savoir lire du code, et mettre des commentaires judicieux dans ce dernier afin d'expliquer les concepts et les actions et non pas les détails d'implémentation redondants avec le code lui-même.
On ne dit pas qu'il faut supprimer la boucle for, arrête de craquer. Juste que la boucle foreach est un gain de clarté quand on fait quelque chose pour chaque élément d'un conteneur.
Et c'est justement là le problème : sur des conteneurs complexes, le "pour chaque" risque de poser un sacré souci... En incluant des choses qui ne devraient pas forcément en faire partie.
Pour ma part, j'ai tendance à n'utiliser de syntaxes genre "foreach" qu'en initialisation / finalisation, de manière à être certain de ne rien oublier. En traitement, je préfère expliciter les intervalles de traitement.
C'est justement là que le foreach est intéressant, puisqu'il se base sur l'itérateur qui a été spécialement défini pour le type de conteneur sur lequel tu te trouves. Si c'est compliqué, tu définis une fois comment se fait un parcours sur ta structure, et après tu restes sur ce parcours. C'est donc bien dans les cas où la structure est compliquée que le foreach est un énorme gain.
Le léger overhead du "foreach" suggèrerait plutôt que le "for" est toujours plus adapté... :mouarf:
Ceci étant dit, l'intérêt primaire du for (au niveau source) est, justement, d'expliciter les bornes. Retrouver un débordement de tableau masqué par un "foreach" n'est pas toujours simple, alors que c'est trivial avec un "for".
C'est le cas que j'ai eu avec des tableaux associatifs, que j'ai indiqué un peu plus haut : la partie "map" du tableau a été parcourue en même temps que la partie "vecteur" via le "foreach", alors que la map aurait été correctement ignorée avec un "for".
Ce qui fait qu'il n'est pas simple de retrouver le débordement de tableau dans un foreach, c'est justement qu'il n'y en a pas...
En même temps, si tu ne veux pas accéder à tous les éléments, c'est sûr que dire "pour tout élément", c'est un peu débile non ?
Quel opérateur ? Les méthodes d'itération ? C'est bien ça qu'on fait pour avoir un itérable sur lequel on peut appeler un foreach...
Débordement logique : itérer sur des valeurs qu'il n'était pas souhaitable d'itérer.
Sauf lorsque tu présupposes que tu veux accéder à "tous les éléments" alors que tu n'as pas forcément le contrôle du contenu... Et que des scories peuvent traîner.
Je te le répète : prends le cas d'un conteneur mixte map / vector... Et trouves-moi donc un "++" qui gère les deux en fonction du contexte, sans risque de confusion. Ce genre de structure est fréquente dans les langages de script notamment (Javascript, PHP, LUA, Python, etc...), ce qui peut poser des problèmes assez mignons quand tu t'interfaces avec eux.
Pendant ce temps, je ferais un "for", j'irais vingt fois plus vite, et il n'y aura pas de risque d'erreurs quel que soit la configuration du conteneur : vecteur "pur" (for=foreach, donc), map "pure" (=vide pour le "for"), ou mixte.
Ou un "++" dans un "for". Cela revient exactement au même, quand on sait lire du code.
Le très fameux "débordement de tableau" logique...
Oui j'avoue, quand je préconise d'utiliser un foreach, c'est que je présuppose que c'est pour tout élément.. Et dans ton exemple récurent, ou que tu expliques que tu ne sais pas ce qu'il y a dans ton tableau, comment tu vas le parcourir avec une boucle for de toutes façons ?
je tiens à rappeler 2 points :
- nous sommes dans une discussion où le programme lambda n'est pas un exemple acceptable... on s'est placé dans un cadre avec performances et/ou sûreté très élevée(s), donc :
- si les performances sont cruciales, le léger surcoût du foreach est peut-être à éviter... après rien ne dit que statiquement, on ne puisse pas "émuler" cette fonctionnalité à moindre coût en repassant sur le for approprié sur le code qui sera réellement compilé (bien sûr cette introduction à la main sera faite dans les cas où l'on sait que la réécriture automatique fonctionnera). sinon, a priori on s'en passera
- si la sûreté est le seul critère, merci de comparer l'analyse qu'il est possible de faire sur un foreach et sur un for (vérification formelle, calcul d'invariant à la main, etc)
- en ce qui concerne les itérateurs, sachez que des experts C++ comme Alexandrescu considèrent qu'ils devraient disparaître au profit des ranges
Sur le cas du parcours complet de tableau par exemple, il apparaît trivial de rétablir le niveau de performance d'un for(), si ce n'était pas le cas dans une version de .Net, je suspecte que ce ne l'est plus maintenant et il n'y a aucune raison fondamentale que cette disparité existe. Dans le cas d'un itérateur, je ne vois aucune raison pour que la boucle for() amène un surcroit de performance.
Dans les autres emplois de for()... Je ne vois pas pourquoi on utiliserait un foreach() pour ces cas.
En bref, foreach() ou for() sont a priori équivalent en performances lorsqu'ils peuvent être utilisés tous deux raisonnablement, il faut juger les performances implémentation par implémentation et prendre ses décisions en fonction du rapport "pénalité en performance/amélioration de la lisibilité" (critère subjectif, et propre au domaine). Si foreach() et for() sont équivalents comme ils devraient l'être, je ne vois pas de raison de préférer for() à foreach() comme je vais m'en expliquer maintenant :
L'avantage du foreach() c'est qu'il fait exactement ce qu'on lui demande (parcourir intégralement un conteneur/itérateur) de façon immédiatement lisible. Donc en assumant que l'itérateur soit écrit correctement, on est garanti de bien le parcourir (je fais ici abstraction des problèmes des conteneurs mutable en présence de threads, le problème est le même pour for ou foreach, encore que foreach pourrait locker automatiquement le conteneur dans une implémentation hypothétique). Un for() écrit dans ce but peut contenir des erreurs à plusieurs points (affectation, test de fin, next) et n'apporte aucune garantie supplémentaire.
Il suffit de vérifier la correction de l'itérateur pour être assuré de la correction de tous les foreach l'utilisant alors que pour être certain de la correction de tous ces parcours, il faudrait vérifier chaque for() employant cet itérateur de façon ad-hoc.
Pour un tableau, le problème est encore plus flagrant : il est aisé de faire des erreurs de bornes dans un for() (je n'en fait qu'extrêmement peu et je ne doute pas que MacLak ou Souviron n'en fasse encore moins mais la perfection n'est pas de ce monde) alors qu'un foreach() ne faillira jamais à sa tâche si l'implémentation de sa compilation est correcte (et il est peu probable qu'elle ne le soit pas sans que cela soit détecté très vite, encore qu'apparemment on n'aie pas les même garanties sur ses performances).
Je répète encore une fois que mon argumentation ci-dessus n'est valable que pour les cas où l'emploi de foreach() est possible sans contorsion, for() est plus flexible et doit être l'outil de choix pour parcourir une partie d'un conteneur ou autre tâche non-réalisable par foreach(), celui-ci doit être réservé au parcours intégral de conteneur/itérateur.
Tu emploies ici des termes techniques propres au C++, les ranges seraient sans doute considéré comme des "itérateurs" dans un autre langage, même s'ils viennent corriger un défaut des itérateurs employées en C++ jusqu'ici.
--
Jedaï
j'emploie le terme itérateur au sens POO, et aucune confusion n'est possible puisque je mets un exemple (d'ailleurs ce n'est pas spécifique au C++, ça marche aussi en Java, D... sous des formes différentes, mais avec les mêmes idées de base)
"corriger un défaut" n'est pas le terme exact, je dirais juste qu'ils permettent d'obtenir une abstraction plus dissociée de la notion de pointeur que les itérateurs de la POO devaient abstraire à l'origine ;)
mais à quel prix ? je suis encore dubitatif (surtout quand on voit un exemple racontant qu'il est impossible de faire un itérateur sur un arbre :?)
Quel exemple ? Par ailleurs à propos des ranges, je n'ai rien trouvé de particulièrement concluant avec une recherche superficielle excepté qu'elles insistaient sur des conteneurs ordonnés et qu'elle permettaient de prendre une "tranche" d'un tel conteneur. Tu aurais des liens plus précis à nous proposer et une explication d'en quoi elle ne sont pas des itérateurs ?
--
Jedaï
Tiens, un vieux concept revenant au goût du jour... Point fort du Pascal et de l'Ada, ça, les intervalles.
Ce qui demande une preuve : soit un KB Microsoft décrivant la correction, soit une série de benchs montrant que le "foreach" est désormais traduit à l'identique d'un "for" quel que soit le cas d'utilisation.
C'est pourtant le cas, d'après le bench effectué... Ce n'est pas parce que conceptuellement, c'est "presque" la même chose que ça l'est réellement une fois traduit en code machine. Cela peut être lié à une variante d'inlining, à une passe différente d'optimisation, à un séquencement légèrement différent de la traduction, ou encore à une occupation plus importante des registres avec le "foreach", obligeant alors à utiliser un peu plus la pile (qui est plus lente qu'un registre).
Bref, des raisons pour cette disparité, il y en a plein, toutes aussi valables les unes que les autres. Cela ne justifie pas le fait que la traduction ne soit pas identique pour un parcours identique, bien sûr, mais cela explique pour quelles raisons il peut y avoir une différence.
Encore faut-il que cela soit vrai... Prenons le cas d'un tableau dynamique, qui serait rempli par un autre thread suivant un algo FIFO (= en fin de tableau).
Si ta boucle est effectuée via un "foreach", que se passe-t'il ? Entre-t'on en boucle infinie si la production est supérieure en vitesse à la consommation, pour cause d'itérateur "end()" jamais atteint ? Si oui, c'est un problème.
S'arrête-t'on avant, car le "end()" a été évalué précédemment ? Si oui, c'est un plus gros problème encore, car le "foreach" viole alors sa postcondition (traiter TOUS les éléments).
Via un "for", tu récupères à un instant T le nombre d'éléments du tableau (donc "tous" en début de boucle), puis tu les consommes, quel que soit le nombre d'éléments ajoutés au tableau pendant la consommation. Ton algorithme s'arrête, pas de boucle infinie, les contextes d'exécution multitâche ne sont pas bloqués et tout va bien.
C'est ça, la supériorité de décider soi-même de ce que l'on fait, au lieu de laisser la machine décider à ta place. C'est la même chose qu'avec les casts implicites : c'est mal, si cast il doit y avoir, alors c'est au programmeur de décider lequel... Ne serait-ce que pour assurer un fonctionnement constant, notamment en cas de changement d'implémentation de l'implicite ou de portage.
L'itérateur est le même pour "foreach" et "for" : s'il est buggé, il plantera pareil pour l'un ou pour l'autre. Ou ta boucle "for" ne bouclera carrément pas du tout si tu oublie d'itérer, ce qui est quand même un bug plutôt visible (et surtout, un bug d'ultra débutant...).
Tu viens justement de citer le maître-mot : flexibilité. Ou, pour utiliser un synonyme adapté au contexte, déterminisme de fonctionnement. Le "for", on le contrôle de A à Z, le "foreach", non.
C'est pour cette raison que je n'utilise pas de "foreach" en dehors des initialisations / finalisations, c'est à dire dans un état particulier de l'application où les interactions entre threads, processus et machines n'existent pas encore... Cas très limité s'il en est, car cela revient à dire que l'on fait un programme monothread, ne communiquant pas, n'interagissant pas avec l'utilisateur, ni avec les autres processus... Un "Hello world" ou peu s'en faut, donc, dans tous les cas quelque chose de potentiellement peu complexe et/ou peu utile...
Dans ce cas précis, il permet d'effectuer un traitement sur tous les éléments sans en oublier (mais aucun ne peut être ajouté / retiré pendant l'itération non plus, hein...), et ne demande pas de modifier le code en cas d'ajout ou suppression d'un élément.
Dans tous les autres cas, du moment que le cardinal du conteneur peut évoluer, c'est une plaie potentielle... Et un lock serait trop coûteux en temps CPU, alors que ce serait la seule solution pour le rendre "fiable". Dans le même temps, un "for" permet d'obtenir quelque chose de sécurisé et fiable sans utiliser le moindre lock... Donc, plus performant, plus sûr, que des avantages, quoi...
Tu penses que le "foreach" est supérieur sur un parcours complet parce que tu n'as manifestement pas l'habitude de déterminer l'environnement d'une fonction impérative, ni les impacts potentiels d'un parcours rendu rigide au point d'être néfaste. Programmer en impératif, cela ne se fait pas avec des concepts et un raisonnement fonctionnels.
Pas tout à fait... Un range, en Pascal ou Ada, est quelque chose qui est soit figé par compilation (domaine de validité d'un type, par exemple, ou bornes d'un tableau statique), soit déterminé à la compilation (bornes d'un tableau dynamique notamment).
Dans les deux cas, itérer dessus se fait de façon particulière, propre à ce concept, et est considéré comme "la bonne manière" d'itérer sur une plage.
Cela marche notamment pour les ensembles et les énumérations, qui, je le rappelle, n'ont pas de valeur autre que symbolique dans ces deux langages. L'autre manière de les utiliser est l'usage de "Pred" et "Succ", qui seraient implémentés en C++ par surcharge de "++" et "--" respectivement. Sauf qu'en Pascal et Ada, la surcharge en question n'est ni nécessaire, ni souhaitable : c'est la déclaration de l'ensemble (ou de l'énumération) qui définit l'ordre algébrique des éléments, ainsi que le premier et le dernier, et ceci au moment de la compilation et de l'exécution pour les dépassements d'intervalles.
Ce qui est tout à fait logique, car un arbre peut posséder deux successeurs ou plus, et non pas un seul... Ce qui empêche totalement le concept de Ranges d'être appliqué.
Les ranges s'appliquent sur tout conteneur, domaine, intervalle linéaire (ou unidimensionnel, au choix), qu'il contienne ou non des trous, tant qu'il est possible de définir une sorte de "groupe" (au sens mathématique du terme) dessus.
L'avantage qu'ils ont par rapport aux itérateurs, c'est qu'une grande partie du contrôle peut être faite au niveau de la compilation, et non pas uniquement à l'exécution. En dehors de ça, ils peuvent bien entendu s'appliquer à tout ou partie de l'objet ciblé, y compris par accès direct contrairement à certains itérateurs "purs" qui fonctionnent exclusivement par parcours séquentiel depuis le début du conteneur.
Donc, en résumé, un range est supérieur à un itérateur parce que :
- Il est résolu en partie à la compilation, parfois même totalement.
- Il permet un accès direct [aléatoire] aux éléments internes, et ne requiert pas de parcours systématique grâce à la résolution partielle au niveau compilation.
Leur inconvénient majeur : impossibilité stricte d'avoir plus d'un successeur, et plus d'un prédécesseur. Ce qui rend effectivement impossible leur utilisation pour un arbre (mais ce serait éventuellement possible sur une chaîne de l'arbre, bien que l'intérêt soit limité).
Mais pour autant que je sache, personne n'a dit que foreach devait dans tous les cas remplacer for. Juste que c'est une syntaxe agérable et confortable à lire lorsqu'on itère une collection ou un tableau en faisant une action spécifique avec chacun des éléments.
Dans mes codes, ne devoir traiter qu'un intervalle plutôt que la totalité d'une collection, ça arrive plus rarement (je parle pour moi).
Il faut savoir aussi que foreach fonctionne sur tous types de collections, ce qui inclut les bags, les queues, les sets, les maps et pas seulement les listes indexées. Rien qu'on peut pas faire avec un for() en étant prudent en cas de listes chaînées à ne pas refaire le parcours depuis le début à chaque itération.
2 choses, est-ce que la majorité des scénarios de parcours itératifs se font dans des contextes de concurrences? Dans mon cas quotidien la réponse est clairement non.Citation:
Via un "for", tu récupères à un instant T le nombre d'éléments du tableau (donc "tous" en début de boucle), puis tu les consommes, quel que soit le nombre d'éléments ajoutés au tableau pendant la consommation. Ton algorithme s'arrête, pas de boucle infinie, les contextes d'exécution multitâche ne sont pas bloqués et tout va bien.
Pour ce qui est du parcours de X éléments d'une file d'attente par un thread consommateur à l'aide d'un for, tu n'as guère beaucoup de garantie si des éléments peuvent être enlevés de la listes des tâches derrière ton dos, soit par une action utilisateur, une modification prioritaire, ou un autre thread qui consomme la file.
En gros il faudra une structure telle qu'une queue bloquante, et c'est des opérations du style push et pop individuelles qui seront utilisées et non des parcours.
En fait, comparer for et foreach en les mettant dans des contextes ou ils ne sont clairement pas adaptés, ça vaut pas la peine.