N’utilisez pas les boucles brutes

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 forwhile 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

Laisser un commentaire