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

Votre commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l’aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google

Vous commentez à l’aide de votre compte Google. Déconnexion /  Changer )

Image Twitter

Vous commentez à l’aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l’aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s