Les exceptions sont des goto déguisés

Article original : Exceptions are just fancy gotos | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

À propos de goto

Retour aux bases : pourquoi est-ce que les goto sont mauvais ?

Pour répondre à cette question, il me suffirait de vous jeter un lien vers l’article de E. Dikjstra, Go To Statement Considered Harmful (arizona.edu), qui explique plutôt bien pourquoi les goto sont l’incarnation du mal, mais j’ai envie d’aller un peu plus loin.

Je vais donner ici une liste de raison pour lesquelles il ne faut pas utiliser goto, résumant ainsi l’article susmentionné tout en y apportant une touche de modernisation :

  1. Les goto sont des structures de contrôle non structurées, contrairement aux autres structures de contrôle en C ++. Les if et les boucles sont géographiquement liées au bloc de code qu’ils contrôlent (avec leur propre cycle de vie). Quand on les lit, on comprend aisément où le bloc de contrôle commence et où il finit. Les fonctions, qui sont une autre structure de contrôle, sont des points d’accès. Elles suivent un contrat dont le compilateur assure la validité. On leur donne une entrée, elles nous renvoient une sortie. Les goto, eux, ne sont pas structurés : ils correspondent à un aller simple dans le code, sans être géographiquement proche et sans assurer un système d’entrée-sortie comme les fonctions. Cela est sujet à produire du code spaghetti peu maintenable.
  2. Considérant ce que cela implique, le goto est probablement le mot-clé le moins sécurisé du langage. Je ne donnerais pas d’exemple expansif dans cet article, mais le nombre de choses autorisées avec les goto est surprenamment élevé. Si vous n’êtes pas assez vigilant, vous pourrez provoquer des crashs, parfois aléatoires, du jardinage mémoire, des comportements indéfinis, etc.
  3. Ici et maintenant, à l’ère du C ++ moderne, plus personne n’utilise les goto. Ça signifie que presque plus personne n’en est familier. La plupart des développeurs sait qu’il ne faut pas l’utiliser, mais assez peu d’entre eux savent pourquoi. De ce fait, si jamais d’aventure ils en croisent un dans une codebase, ils vont soit tenter de refactorer le code (avec le risque qu’ils comprennent la fonctionnalité de travers et causent une régression), soit le laisser en plan sans essayer de le comprendre. Étant donné qu’il est compliqué à utiliser, le fait que personne n’utilise le goto sur une base régulière est un argument de plus contre son utilisation.

Il existe des contextes spécifiques dans certains langages où le goto peut être considéré comme une bonne pratique, mais pas en C ++ moderne.

À propos des structures de contrôle et du code spaghetti

Dans la section précédente, le premier point (qui constitue l’argument principal contre le goto) parle de « structure de contrôle » et de « code spaghetti ».

De manière imagée, on pourrait voir l’exécution d’un programme comme une corde que l’on déroule le long du code, à mesure qu’il est exécuté. Tant que ce sont des instructions classiques qui sont exécutées, la corde se déroule normalement. Mais lorsqu’on atteint une structure de contrôle, cela se passe un peu différemment.

Quand l’exécution atteint une boucle for ou while, alors la corde effectue elle-même des boucles autour du bloc de code concerné. Quand l’exécution atteint une conditionnelle (if ou else), alors la corde passe au-dessus du code si nécessaire (en fonction de la condition vérifiée) pour continuer juste après.

Quand l’exécution atteint un appel de fonction, alors un brin de la corde se détache, forme un lacet qui va suivre le corps de la fonction puis revenir à la corde, là où elle l’a quitté.

Par contre, quand l’exécution atteint un goto, alors parfois la corde n’aura d’autre choix que d’être tendue à travers tout le code, pour atteindre le label associé. S’il y a plusieurs goto dans le code, alors la corde sera tendue à plusieurs endroits et dans tous les sens.

Si vous avez de bonnes structures de contrôle, alors vous pourrez suivre aisément votre corde du début à la fin, sans jamais être perdu. Mais si vos structures de contrôle sont chaotiques, alors la corde va se croiser partout et partir dans tous les sens. Elle ressemblera alors à un plat de spaghetti. C’est ça qu’on appelle avoir un code spaghetti.

Comme le goto a tendance à tendre la proverbiale corde à travers tout le code, son utilisation est facilement sujette à du code spaghetti.

Mais est-ce que le goto est vraiment malveillant ?

En prenant en compte tout ce que l’on vient de dire, ne pourrait-on pas quand même imaginer une manière sécurisée d’utiliser le goto ? Après tout, goto n’est malveillant que s’il provoque du code spaghetti. Mais si on réalise un code qui n’utilise le goto que localement, dans un espace contrôlé, alors le code ne sera pas spécialement spaghetti-esque, n’est-ce pas ?

Il existe effectivement des designs qui utilisent le goto pour rendre le code plus clair qu’il ne le serait avec d’autres structures de contrôle.

L’exemple le plus classique est celui des boucles imbriquées :Capture1.P

//...
 
bool should_break = false;
for (int i = 0 ; i < size_i ; ++i)
{
    for (int j = 0 ; j < size_j ; ++j)
    {
        if (condition_of_exit(i,j))
        {
            should_break = true;
            break;
        }
    }
    if (should_break)
        break;
}
 
//...

Si on écrit le même code avec un goto, ça sera un peu plus court et plus clair :Capture2.PNG888x314

// ...
 
for (int i = 0 ; i < size_i ; ++i)
{
    for (int j = 0 ; j < size_j ; ++j)
    {
        if (condition_of_exit(i,j))
            goto end_of_nested_loop;
    }
}
end_of_nested_loop:
 
// ...

Vous voyez ? goto n’est pas si inutile que ça !

Du coup, devrait-on utiliser ce mot-clé dans ces cas très spécifiques où il rend le code plus clair ? Je ne pense pas.

Il est au final assez compliqué de trouver des exemples variés où le goto est meilleur que les autres structures de contrôle, et les boucles imbriquées similaires à celle présentée ici sont rares. Et même si, dans cet exemple, le code est plus court, personnellement je ne le trouve pas plus clair pour autant. Il y a 2 niveaux de bloc de différence entre le goto et son label, ce qui à la lecture est contre-intuitif. De plus, dans tous les cas le facteur humain reste un gros problème.

Donc, est-ce que goto est vraiment malveillant ? Non, mais il reste, dans l’absolu, une très mauvaise pratique.

À propos des exceptions

Les exceptions : une manière moderne de casser les structures de contrôle

Les exceptions sont une manière de gérer les cas d’erreur. Elles peuvent aussi être utilisées comme une structure de contrôle standard, vu qu’on peut personnaliser ses propres exceptions, puis les lever et les attraper à loisir.

J’aime bien m’imaginer les exceptions comme étant des if qui pendouillent dans le code : d’une part si tout se passe bien, le code se déroule de manière linéaire, sans accroc, mais d’autre part, si une erreur survient, alors on lance la balle dans les airs en espérant que quelque chose dans les strates supérieures du programme la rattrape.

Les exceptions cassent les structurent de contrôle standard. Reprenons l’image de la corde déroulée le long du code : quand vous appelez une fonction qui peut lever une exception, alors un brin de corde se détache pour exécuter la fonction (comme avant), mais vous n’avez aucune garantie que le brin sera retourné à votre corde à l’endroit où vous avez appelé la fonction. Il pourra être raccordé n’importe où au-dessus, dans la pile d’appel. Le seul moyen d’empêcher ce scénario est de trycatch toutes les exceptions quand vous appelez la fonction, mais c’est un luxe qui n’est possible que si vous savez quoi faire dans tous les cas dégradés possibles.

De plus, quand vous écrivez une fonction dans laquelle vous levez possiblement une exception, vous n’avez aucun moyen de savoir si et où elle sera attrapée.

Même si ce n’est pas aussi mauvais que goto, parce qu’on a plus de contrôle dessus, il est très facile d’écrire du code spaghetti en usant d’exceptions. Avec ce recul, on peut même considérer que les exceptions sont « des goto modernes ».

On peut même écrire l’exemple des boucles imbriquées avec des exceptions :

//...
 
try
{
    for (int i = 0 ; i < size_i ; ++i)
    {
        for (int j = 0 ; j < size_j ; ++j)
        {
            if (condition_of_exit(i,j))
                throw;
        }
    }
}
catch (const std::exception& e)
{ /* nothing */ }
 
//...

Je ne recommanderais pas une telle écriture cependant. Les exceptions sont malfaisantes.

Est-ce que les exceptions sont vraiment malfaisantes ?

… le sont-elles ? Pas vraiment.

Dans la section précédente, j’ai statué sur le fait que les goto ne sont pas à proprement parler malveillantes mais qu’elles sont très sujettes aux fautes et aux erreurs. Elles n’en valent juste pas la peine. C’est pareil pour les exceptions : elles ne sont pas malfaisantes, c’est juste une feature.

En mon humble opinion, contrairement aux goto, il existe des moyens sécurisés d’utiliser les exceptions.

Une différence majeure entre les exceptions et les goto

Pour comprendre comment bien utiliser les exceptions, il faut comprendre les différences qu’elles ont avec le goto.Les exceptions, contrairement aux goto, n’envoient pas l’exécution à un endroit totalement indéterminé, elle envoie l’exécution vers le haut. Cela peut être de quelques blocs (comme dans l’exemple des boucles imbriquées) ou sur plusieurs appels de fonctions.

Quand utiliser les exceptions ?

Envoyer l’exécution du programme vers le haut reste assez indéterminé malgré tout, et dans la plupart des cas cela brise la continuité du programme.

Dans la plupart des cas.

Il existe une situation spécifique où vous voulez que l’exécution se stoppe abruptement : quand quelqu’un utilise une fonctionnalité d’une manière qui provoque un comportement indésirable.

Quand vous écrivez une fonctionnalité, vous voulez avoir la possibilité de tout stopper pour éviter un cas dégradé dangereux, et à la place de finir le traitement renvoyer l’exécution vers le haut en indiquant « Quelque chose d’imprévu s’est produit, je ne peux pas continuer ». Non seulement cela va prévenir les comportements indésirables, mais cela va aussi indiquer à l’utilisateur qu’il n’a pas correctement utilisé la fonctionnalité, le forçant à se corriger ou à gérer le cas d’erreur.

Quand vous rédigez une fonctionnalité, il y a un mur virtuel entre vous et l’utilisateur, avec un petit trou symbolisé par l’interface de la fonctionnalité. C’est à chacun d’entre vous de gérer correctement le comportement de la corde proverbiale de chaque côté du mur.

Une fonctionnalité peut d’ailleurs très bien être une grosse librairie ou une simple classe. Du moment qu’il y a un niveau d’encapsulation, il est valide d’utiliser des exceptions comme faisant partie de l’interface.

Un bon exemple de cela est la méthode at () de std ::vector < >. Le but de cette méthode est de renvoyer le énième élément du vecteur, mais il y a un risque que l’utilisateur demande un élément hors-limite. Dans ce cas, lever une exception est le moyen qu’a std ::vector < > d’empêcher un comportement indéfini. Si l’utilisateur capture l’exception, alors il peut écrire le code à exécuter en cas d’indice hors-limite. Sinon, le programme sera stoppé, lui indiquant qu’il a commis une erreur et le forcera à sécuriser son code ou à gérer le cas dégradé.

Dans tous les cas, vous devez documenter toute exception que vous levez.

Conclusion

En résumé, les bonnes pratiques à employer vis-à-vis des exceptions peuvent se résumer en trois points :

  • N’utilisez pas les exceptions comme des structures de contrôle.
  • Vous pouvez utiliser les exceptions comme partie de l’interface d’une fonctionnalité.
  • Vous devez documenter toute exception que vous levez.

Merci de votre attention et à la semaine prochaine !

Article original : Exceptions are just fancy gotos | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Laisser un commentaire