Article original : Don’t use raw loops | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre
Note d’intention
Sean Parent a un jour dit « No raw loops », pas de boucle brute. C’Ă©tait il y a huit ans.
Aujourd’hui, si vous n’avez pas un bagage C++ extrĂŞmement fort, vous n’avez sans doute jamais entendu cette maxime (ni mĂŞme entendu parler de Sean Parent). cela fait qu’aujourd’hui, en 2021, beaucoup de projets C++ utilisent des tonnes de boucles brutes et pratiquement aucun algorithme.
Cet article est destiné à vous, les développeuses et développeurs qui utilisez des boucles brutes en 2021, et va expliquer pourquoi et comment utiliser des algorithmes à la place.
Ressources
Ce sujet a Ă©tĂ© couvert par nombre d’experts ces dernières annĂ©es. Si vous vous sentez d’attaque pour du contenu très technique et en anglais, suivez les liens suivants :
L’exposĂ© de Sean Parent oĂą il parle (en autres) des boucles brutes : C++ Seasoning | GoingNative 2013 | Channel 9 (msdn.com)
Un exposé de Jason Turner qui parle de « code smells » (ce qui inclue les boucles brutes) :
C++ Code Smells – Jason Turner – CppCon 2019 – YouTube
L’exposĂ© de Jonathan Boccara Ă propos des algorithmes de la STL : CppCon 2018: Jonathan Boccara “105 STL Algorithms in Less Than an Hour” – YouTube
En bonus, une carte (littérale) où sont représentés les algorithmes de la STL :
The World Map of C++ STL Algorithms – Fluent C++ (fluentcpp.com)
Les boucles brutes : c’est quoi ?
Ce que j’appelle boucles brutes ou dans leur version originale raw loops sont les boucles telles que vous les imaginez : les mots-clĂ©s for
, while
ou encore do while
rattachés à des blocs de code.
Les boucles brutes sont opposĂ©es aux algorithmes, qui sont des boucles (ou pas ?), encapsulĂ©es dans des fonctions. Vous appelez ensuite ces fonctions avec tels ou tels arguments en fonction de l’usage que vous voulez faire de cet algorithme.
Pourquoi vous ne devriez pas utiliser les boucles brutes
C’est une question de sĂ©mantique. Une boucle brute n’exprime pas un but, mais une manière technique de procĂ©der. Elles expriment comment votre algorithme fonctionne.
Quand vous appelez un algorithme, vous décrivez une intention, vous écrivez ce que vous voulez obtenir.
Par exemple, jetons un Ĺ“il Ă ce morceau de code :
//...
for (size_t i = 0; i < my_vect.size() && predicate(my_vect, i) ; ++i)
{
//...
}
//...
Cela vous indique que le programme effectue une boucle, indexée sur un vector et contrôlée par un prédicat personnalisé. Mais ça ne dit pas ce que la boucle réalise et quel est le résultat attendu : vous devez plonger dans le corps de la boucle pour le savoir.
Regardons maintenant ce morceau de code :
//...
auto it_found = std::find_if(cbegin(my_vect), cend(my_vect), predicate);
//...
MĂŞme si vous ne savez pas comment find_if()
fonctionne en interne, vous comprenez aisĂ©ment qu’elle revoie un Ă©lĂ©ment de my_vect
qui vérifie la condition predicate()
. Vous ne savez pas comment ça marche, mais vous savez ce qui est renvoyé.
Ă€ partir de lĂ , il faut prendre en compte plusieurs points :
- Les algorithmes Ă©lèvent le niveau d’abstraction et vous aide Ă comprendre les intentions cachĂ©es derrière les lignes de code.
- Une sémantique adéquate améliore la lisibilité, une meilleure lisibilité rend le code plus maintenable, un code mieux maintenable est moins sujet à des régressions.
- Appeler un algorithme est souvent moins verbeux que le réécrire.
- Les boucles pures sont sujette à des erreurs assez communes, comme les dépassements-de-un, les boucles vides, la complexité naïve, etc.
Que faire quand il n’existe pas d’algorithme adaptĂ© ?
Il arrive parfois qu’aucun algorithme existant ne corresponde parfaitement Ă votre besoin. Dans ce cas-lĂ , que faire ?
Combiner des algorithmes pour en faire un nouveau
Souvent, votre algorithme spĂ©cifique est simplement une combinaison de deux algorithmes existants ou une spĂ©cification d’un algorithme dĂ©jĂ existant. Dans ce cas, il suffit de l’implĂ©menter dans une fonction dĂ©diĂ©e, en prenant bien soin de lui donner un nom clair et explicite.
Par exemple : vous devez vérifier que, dans un vector, tous les éléments qui remplissent la condition A
remplissent aussi la condition B
. Pour faire cela, vous pouvez utiliser l’algorithme std::all_of()
avec un prédicat personnalisé :
template< typename Iterator, typename PredA, typename PredB >
bool both_or_none( Iterator first, Iterator last, PredA & predA, PredB & predB )
{
auto pred = [&predA,&predB](const auto& elt)
{
return predA(elt) == predB(elt);
};
return all_of(first, last, pred);
}
Le corps de cet algorithme est assez court : il crĂ©e une fonction combine nos deux prĂ©dicats pour implĂ©menter notre condition spĂ©cifique, puis applique l’algorithme std::all_of()
, qui vérifie que cette condition est vraie pour tous les éléments de la collection.
Enrober une boucle pure dans une fonction
Si vraiment vous n’avez aucun moyen de combiner les algorithmes qui ne soit pas trop forcĂ© ou qui ne fasse pas artificiel, vous n’avez plus qu’Ă implĂ©menter votre boucle brute en l’encapsulant dans une fonction dĂ©diĂ©e.
Ainsi, vous aurez implĂ©mentĂ© votre propre algorithme, qui pourra ĂŞtre appelĂ© par quiconque en a besoin. N’oubliez pas de lui donner un nom clair et explicite.
Par exemple: vous avez une collection et avez besoin de savoir, parmi tous les Ă©lĂ©ments qui respectent une condition donnĂ©e, lequel est le plus grand d’entre eux. En somme, cela correspondrait Ă l’algorithme max_if()
s’il existait.
Ce comportement est compliquĂ© Ă implĂ©menter en n’utilisant que la STL, car il faudrait rĂ©ussir Ă dĂ©tacher le sous-ensemble de la collection qui valide la condition pour pouvoir lui appliquer l’algorithme std::max()
derrière. Hors, le seul algorithme permettant de faire cela est std::copy_if()
, qui copie les éléments. Or, les copies peuvent être coûteuses, donc on ne veut pas faire ça.
Que faire alors ? Écrivons une boucle qui implémente ce max_if()
nous-mĂŞme, en l’encapsulant correctement :
template< typename Iterator, typename Pred >
constexpr Iterator max_if( Iterator first, Iterator last, Pred & pred )
{
Iterator max_element = last;
for (auto it = first ; it != last ; ++it)
{
if (pred(*it) && (max_element == last || *it > *max_element))
max_element = it;
}
return max_element;
}
Dans le reste du programme, l’utilisation de max_if()
sera sĂ©mantiquement explicite, avec tous les avantages qu’apportent les algorithmes.
Quelques exemples d’algorithmes de la STL
Il y a beaucoup d’algorithmes dans la STL. Je vous suggère d’ĂŞtre curieux(se) et de l’explorer par vous-mĂŞme : Algorithms library – cppreference.com
En tant que mise en bouche, voici une petite liste d’algorithme que, si vous ne les connaissez pas dĂ©jĂ , vous devriez apprendre Ă connaĂ®tre :
- std::find() : Recherche un élément égal à une valeur donnée.
- std::find_if() : Recherche un élément qui vérifie une condition donnée.
- std::for_each() : Applique une fonction donnée à tous les éléments de la collection.
- std::transform() : Applique une fonction donnée à tous les éléments de la collection et stocke le résultat dans une autre collection.
- std::all_of() : Vérifie si tous les éléments de la collection vérifient un prédicat donné.
- std::any_of() : Vérifie si au moins un élément de la collection vérifient un prédicat donné.
- std::copy_if() : Copie les Ă©lĂ©ments de la collection s’ils vĂ©rifient une condition donnĂ©e.
- std::remove_if() : Enlève le premier élément de la liste qui vérifie une condition donnée.
- std::reverse() : Inverse l’ordre des Ă©lĂ©ments dans la collection.
- Et bien plus…
Si vous voulez aller plus loin, il y a une prĂ©sentation d’une heure qui prĂ©sente plus de cent algorithmes de la STL : CppCon 2018: Jonathan Boccara “105 STL Algorithms in Less Than an Hour” – YouTube
En conclusion
Beaucoup d’experts en C++ sont d’accord pour dire que les boucles sont vouĂ©es Ă disparaĂ®tre dans les plus hautes couches d’abstraction, et ne seront utilisĂ©es que pour les algorithme de plus bas niveau. Cette dĂ©claration n’est pas absolue, mais un but Ă poursuivre, un idĂ©al Ă garder en tĂŞte quand on code.
Si comme beaucoup de dĂ©veloppeur(se)s C++ vous avez tendance Ă utiliser des boucles brutes au lieu d’algorithmes, vous devriez aller faire un tour dans les ressources que j’ai mentionnĂ©es au dĂ©but de l’article. Comme vous vous familiariserez avec eux et les utiliserez de plus en pratique, vous les trouverez de plus en plus commodes.
Merci pour votre attention et Ă la semaine prochaine !
Article original : Don’t use raw loops | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre