Le code et ses raisons : goto en C
#.
Par rz0 dans Le code et ses raisons. Publié le 10.09 2009 à 13:06.
14 commentaires.
<.
Conventions : le retour
>.
Le code et ses raisons : typedef en C
Tak tak, aujourd’hui Ours répond ! Quelqu’un m’a demandé hier pourquoi
mon code contenait autant d’instructions goto, la malfamée. Comme
c’est une question qui revient souvent, je me suis dit que cela
pouvait intéresser notre audience moins expérimentée en C. Here
comes!
Ahem, commençons. goto, c’est un truc un peu vite fait obscur du C,
un peu comme les trucs Caml obscurs de bluestorm mais
quand même pas au même niveau de louchitude. goto, ça déchaîne les
jeunes comme les vieux, tout ça au final pour une pauvre
instruction qui sautille. Bref, goto ça transporte le flot
d’exécution d’un endroit à un autre d’une fonction. Ça, c’est la
description objective, notre ptit Get the Facts avant
d’embrailler sur la vraie question : pourquoi diable utilise-t-on
goto ? Je ne pourrai pas répondre pour « on » mais je peux répondre
pour moi !
goto est avant tout l’instrument du pauvre. Voilà qui est dit. La
raison principale derrière l’utilisation de goto est que celui-ci
permet d’émuler des flots d’exécution atypiques et indisponibles en
C. On parle souvent de gestion des erreurs, d’exceptions du pauvre ;
ce n’en est qu’un cas particulier. L’idée générale est celle que je
viens d’énoncer : goto permet d’écrire des programmes C avec des
structures non présentes directement dans le langage.
Lol le C, c’est pour les ptits joueurs, ya pas d’exceptions
Quelques exemples maintenant, avec tout d’abord le grand classique : la sortie de boucles imbriquées et la gestion des erreurs. Pourquoi ai-je donc regroupé ces deux cas qui apparaissent distincts ? Car ils modèlent tous deux un type de flot permis par les exceptions dans les langages qui en possèdent : on dispose de contextes imbriqués desquels on veut se défaire en un saut.α
α : Notez que je n’ai pas dit que les exceptions pouvaient être
émulées par des goto, juste qu’il y a des ressemblances dans le
flot obtenu.
Jetons un œil à deux bouts de code. La sortie de bloc, tout d’abord :
{
while (a) {
while (b) {
if (c)
goto end;
...
}
}
end:
...
}
Et la gestion des erreurs, dans sa forme modelant une pile :
{
if ((a = f(...)) == NULL)
goto bada;
if ((b = g(...)) == NULL)
goto badb;
...
return ...;
badb:
antif(a);
bada:
return ...;
}
Le premier exemple ne devrait demander aucun commentaire. Le second n’est pas aussi évident : il n’exhibe pas clairement l’idée de contextes imbriqués. Celle-ci est en vérité masquée par le court-circuitage introduit par les sauts. Nous aurions pu écrire :
{
if ((a = f(...)) != NULL) {
if ((b = g(...)) != NULL) {
...
} else {
antif(a);
goto bada;
}
} else {
bada:
return ...;
}
return ...;
}
OK, le tout s’est sensiblement enlaidi, et je vous rassure, personne
ne l’écrit ainsi (car en plus d’être laid, on effectue beaucoup plus
de sauts en cas d’erreur). L’avantage de cette forme, cependant, est
qu’elle modélise beaucoup plus précisément le comportement d’une
exception dans un langage de plus haut niveau : chaque bloc if
correspondrait à un bloc try (ou équivalent) avec une clause de
finalisation (contenue dans la partie else). Lorsqu’une condition
exceptionnelle se produit, les clauses de finalisation sont déroulées
les unes après les autres, dans l’ordre d’empilement des contextes
(représentés ici par les blocs).
À part remarquer que la gestion des erreurs, cas normal d’utilisation
des exceptions, n’est pas celui qui possède la correspondance la plus
naturelle avec l’utilisation de goto, nous n’avons plus grand chose
à dire ici. Passons donc à…
Plus tiré par les cheveux (encore) : appels récursifs terminaux
Si vous jetez un œil aux codes sources que j’écris, il arrive que
j’utilise des goto à la place de boucles classiques. Et oui,
sacrilège § L’explication ? ’Tention, c’est tordu : cela modélise des
appels récursifs terminaux simples.β
L’exemple typique est celui d’une fonction :
{
T x;
re:
if (...)
...
if (...)
...
if (...) {
x = f(x, ...);
goto re;
}
}
Alors oui, on pourrait bien sûr écrire tout ça sous forme d’une boucle ou carrément utiliser l’appel récursif et prier pour que le compilateur C optimise.γ
C’est en vérité une question d’emphase, et ces questions là sont
profondément subjectives. Une boucle mettrait moins l’accent sur
l’action et davantage sur sa répétition. Dans le cas où le cas
récursif est marginal, la construction s’en retrouve relativement
maladroite : on a alors le choix entre une condition de boucle obscure
et redondante, des marqueurs booléens dans tous les sens, ou des
instructions break et continue judicieusement placées. En
choisissant cette dernière solution, qui semble la moins
contraignante, on se retrouve avec un code dans ce genre-là :
{
T x;
for (;;) {
if (...)
...
if (...)
...
if (...) {
x = f(x, ...);
continue;
}
break;
}
}
Mieux ? Je ne crois pas, mais je n’irai pas non plus cracher sur celui qui écrit cela. :) Tout cela au fond pour une question de…
β : Là aussi, soyons fous mais pas trop, on ne peut pas émuler la
récursivité terminale dans le cas général avec goto.
γ : GCC le fait, m’enfin, d’autres compilateurs ne le font pas forcément, ce n’est pas une optimisation très orthodoxe en C comme elle peut l’être dans les langages fonctionnels, l’idiome étant en Cδ de tout convertir en style itératif.
δ : Mais rms a trop fait de Lisp, comme tout le monde sait. Lalala.
Lisibilité !
C’est là que les gens se fâchent, que les regards se clashent, que
la violence se lâche, wash wash. Hum, les uns disent de ne pas
utiliser goto, pour la clarté du code, et pour les mêmes raisons,
d’autres l’utilisent ! C’est la vie. Après tout, il y bien des gens
qui codent en style GNU en pensant se rendre lisibles. :)
La question de la lisibilité de goto tient principalement à sa
nature la plus primaire : la possibilité de passer librement d’une
portion de code à une autre. Les détracteurs de goto pointent du
doigt les utilisations de l’instruction qui engendrent des transitions
difficiles à suivre dans le flot. Ceux qui en défendent le gain visuel
évoquent la possibilité offerte par goto de déporter des blocs de
code d’un endroit à un autre. Cette astuce est principalement utilisée
pour la gestion des erreurs et permet ainsi de dégager du traitement
utile la gestion des cas de défaillance, rares en pratique et souvent
triviaux (l’action à entreprendre étant, la plupart du temps, de faire
remonter l’information telle qu’on la reçoit).
Pour conclure sur un dernier exemple, j’abuse souvent de cette propriété pour écrire des choses comme celles-ci :
{
T *p;
R *q;
p = malloc(sizeof *p);
q = malloc(sizeof *q);
if (p == NULL || q == NULL)
goto bad;
...
return ...;
bad:
free(q);
free(p);
return ...;
}
Mon style vous répugne ? N’hésitez pas à commenter. :)
Annexe : setjmp et ses copains
Si, de mon expérience, goto est généralement relativement bien reçu
dans la communauté des programmeurs C dans son ensemble (excepté dans
certains cercles, et dans l’enseignement, sans doute de peur de
pervertir les étudiantsε), il en est autrement de setjmp, le
« goto non local ».ζ
Il y a probablement plusieurs raisons à cela : la difficulté
d’utilisation (cf. restrictions énoncée par la norme, dont la
nécessité de déclarer toutes les variables locales manipulables après
le saut volatile), mais aussi les divers problèmes lié à la
conservation et la restauration des états : il faut pouvoir replacer
de manière sûre le contexte au point où il était au moment de l’appel
à setjmp. Cela implique la libération de mémoire et l’appel de
destructeurs pour les données allouées dans l’intervalle, mais
également des notions de plus bas niveau telles que la gestion des
signaux (cf. sigsetjmp(3)). En plus de ça, setjmp avec toute
cette complication engendre des performances moisies, en général.
setjmp est toutefois utilisé, parfois, pour implémenter des
mécanismes d’exceptions, notamment dans la kazlibη (pour le C) et
diverses implémentations portables de langages dynamiques
(l’interpréteur interactif d’OCaml, si je me rappelle bien, et
d’autres dont je ne me rappelle plus…).
ε : Il paraît que ce sont les éléments perturbateurs tels que moi qui tentent leurs petits camarades avec le côté obscur. :)
ζ : Notez que GCC possède également des goto non locaux et
indirects grâce aux pointeurs d’étiquettes. Je ne m’étendrai pas
à ce sujet (car je connais mal le GNU C). Plus d'informations dans
le manuel de GCC.
η : Je ne vois plus dans Google la page Web correspondant. Le projet est peut-être bien décédé…
Annexe : omg l’optimisation qu’elle est bonne
On entend dire parfois que goto c’est pas bon pour les
performances. Bon alors, dans l’absolu, hum, en fait il n’y a pas
d’absolu. On pourrait dire que remplacer des structures de contrôle
classiques par goto enlève de l’information sémantique, et bon, dans
l’absolu, c’est vrai. La question est de savoir si cette information
sémantique est pertinente d’une part, et si elle est utilisée, d’autre
part. Question pertinence, c’est, là encore, un peu subjectif : est-ce
que tel ou tel goto remplace simplement une combinaison intelligente
de structures de contrôle ou ne reflète-t-il un concept absent du C ?
C’est davantage une question philosophique, donc humaine, que
d’importance à la machine… car en vérité, la plupart des
compilateurs modernes ne tiennent absolument pas compte de ce genre de
choses, et le langage intermédiaire employé n’emploie que des
sauts. Ce sont ensuite les algorithmes d’analyse de flot (brrr,
théorie des graphes) qui ont à leur charge de différencier les blocs
et autres constructions utiles au compilateur.