Cette question est en fait le résultat d'une discussion intéressante sur programmation.reddit.com il y a quelque temps. Cela se résume essentiellement au code suivant:
int foo(int bar)
{
int return_value = 0;
if (!do_something( bar )) {
goto error_1;
}
if (!init_stuff( bar )) {
goto error_2;
}
if (!prepare_stuff( bar )) {
goto error_3;
}
return_value = do_the_thing( bar );
error_3:
cleanup_3();
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
L'utilisation de goto
here semble être la meilleure voie à suivre, aboutissant au code le plus propre et le plus efficace de toutes les possibilités, ou du moins il me semble. Citant Steve McConnell dans Code Complete :
Le goto est utile dans une routine qui alloue des ressources, effectue des opérations sur ces ressources, puis désalloue les ressources. Avec un goto, vous pouvez nettoyer dans une section du code. Le goto réduit la probabilité que vous oubliez de désallouer les ressources à chaque endroit où vous détectez une erreur.
Une autre prise en charge de cette approche provient du livre Linux Device Drivers , dans cette section .
Qu'est-ce que tu penses? Ce cas est-il une utilisation valide pour goto
en C? Préférez-vous d'autres méthodes, qui produisent du code plus compliqué et / ou moins efficace, mais à éviter goto
?
la source
on error goto error
système de gestion des erreurs :)Réponses:
FWIF, je trouve que l'idiome de gestion des erreurs que vous avez donné dans l'exemple de la question est plus lisible et plus facile à comprendre que l'une des alternatives données dans les réponses jusqu'à présent. Bien que ce
goto
soit une mauvaise idée en général, elle peut être utile pour la gestion des erreurs lorsqu'elle est effectuée de manière simple et uniforme. Dans cette situation, même s'il s'agit d'ungoto
, il est utilisé de manière bien définie et plus ou moins structurée.la source
En règle générale, éviter goto est une bonne idée, mais les abus qui prévalaient lorsque Dijkstra a écrit pour la première fois `` GOTO considéré comme nuisible '' ne traversent même pas l'esprit de la plupart des gens comme une option ces jours-ci.
Ce que vous décrivez est une solution généralisable au problème de gestion des erreurs - cela me convient tant qu'il est soigneusement utilisé.
Votre exemple particulier peut être simplifié comme suit (étape 1):
Poursuivre le processus:
C'est, je crois, équivalent au code original. Cela semble particulièrement propre car le code d'origine était lui-même très propre et bien organisé. Souvent, les fragments de code ne sont pas aussi ordonnés que cela (bien que j'accepte un argument selon lequel ils devraient l'être); par exemple, il y a souvent plus d'états à passer aux routines d'initialisation (setup) qu'indiqué, et donc plus d'états à passer aux routines de nettoyage également.
la source
Je suis surpris que personne n'ait suggéré cette alternative, donc même si la question existe depuis un certain temps, je vais l'ajouter: une bonne façon de résoudre ce problème est d'utiliser des variables pour garder une trace de l'état actuel. Il s'agit d'une technique qui peut être utilisée, qu'elle soit ou non
goto
utilisée pour arriver au code de nettoyage. Comme toute technique de codage, elle a des avantages et des inconvénients et ne conviendra pas à toutes les situations, mais si vous choisissez un style, cela vaut la peine d'être pris en compte - surtout si vous voulez évitergoto
sans vous retrouver avec desif
s profondément imbriqués .L'idée de base est que, pour chaque action de nettoyage qui pourrait être nécessaire, il existe une variable à partir de laquelle nous pouvons dire si le nettoyage doit être effectué ou non.
Je vais d'abord montrer la
goto
version, car elle est plus proche du code de la question d'origine.Un avantage de ceci par rapport à certaines des autres techniques est que, si l'ordre des fonctions d'initialisation est changé, le nettoyage correct se produira toujours - par exemple, en utilisant la
switch
méthode décrite dans une autre réponse, si l'ordre d'initialisation change, alors leswitch
doit être édité très soigneusement pour éviter d'essayer de nettoyer quelque chose qui n'a pas été réellement initialisé en premier lieu.Maintenant, certains pourraient argumenter que cette méthode ajoute un grand nombre de variables supplémentaires - et en effet dans ce cas c'est vrai - mais en pratique, une variable existante suit déjà, ou peut être amenée à suivre, l'état requis. Par exemple, si le
prepare_stuff()
est en fait un appel àmalloc()
, ou àopen()
, alors la variable contenant le pointeur ou le descripteur de fichier renvoyé peut être utilisée - par exemple:Maintenant, si nous suivons en plus l'état de l'erreur avec une variable, nous pouvons éviter
goto
complètement, et toujours nettoyer correctement, sans avoir d'indentation qui devient de plus en plus profonde au fur et à mesure que nous avons besoin d'initialisation:Encore une fois, il y a des critiques potentielles à ce sujet:
if (oksofar)
vérifications échouées en un seul saut vers le code de nettoyage (GCC le fait certainement) - et dans tous les cas, le cas d'erreur est généralement moins critique pour les performances.N'est-ce pas ajouter encore une autre variable? Dans ce cas oui, mais souvent la
return_value
variable peut être utilisée pour jouer le rôle quioksofar
joue ici. Si vous structurez vos fonctions pour renvoyer des erreurs de manière cohérente, vous pouvez même éviter la secondeif
dans chaque cas:L'un des avantages d'un tel codage est que la cohérence signifie que tout endroit où le programmeur d'origine a oublié de vérifier la valeur de retour ressort comme un pouce endolori, ce qui facilite la recherche de (cette classe de) bogues.
Donc - c'est (encore) un autre style qui peut être utilisé pour résoudre ce problème. Utilisé correctement, il permet un code très propre et cohérent - et comme toute technique, entre de mauvaises mains, il peut finir par produire du code long et déroutant :-)
la source
Le problème avec le
goto
mot-clé est généralement mal compris. Ce n'est pas du mal. Vous devez juste être conscient des chemins de contrôle supplémentaires que vous créez avec chaque goto. Il devient difficile de raisonner sur votre code et donc sur sa validité.FWIW, si vous recherchez des didacticiels developer.apple.com, ils adoptent l'approche goto pour la gestion des erreurs.
Nous n'utilisons pas de gotos. Une importance plus élevée est accordée aux valeurs de retour. La gestion des exceptions se fait via
setjmp/longjmp
- tout ce que vous pouvez.la source
Il n'y a rien de moralement mal dans la déclaration goto, pas plus qu'il n'y a quelque chose de moralement faux avec les pointeurs (void) *.
Tout dépend de la façon dont vous utilisez l'outil. Dans le cas (trivial) que vous avez présenté, une déclaration de cas peut atteindre la même logique, mais avec plus de surcharge. La vraie question est: "quelle est ma vitesse requise?"
goto est tout simplement rapide, surtout si vous faites attention de vous assurer qu'il se compile en un saut court. Parfait pour les applications où la vitesse est une prime. Pour les autres applications, il est probablement judicieux de prendre la surcharge avec if / else + case pour la maintenabilité.
Rappelez-vous: goto ne tue pas les applications, les développeurs tuent les applications.
MISE À JOUR: Voici l'exemple de cas
la source
GOTO est utile. C'est quelque chose que votre processeur peut faire et c'est pourquoi vous devriez y avoir accès.
Parfois, vous voulez ajouter un petit quelque chose à votre fonction et vous pouvez le faire facilement. Cela peut gagner du temps.
la source
En général, je considérerais le fait qu'un morceau de code pourrait être le plus clairement écrit en utilisant
goto
comme un symptôme que le déroulement du programme est probablement plus compliqué que ce qui est généralement souhaitable. Combiner d'autres structures de programme de manière étrange pour éviter l'utilisation degoto
permettrait de traiter le symptôme plutôt que la maladie. Votre exemple particulier pourrait ne pas être trop difficile à mettre en œuvre sansgoto
:mais si le nettoyage n'était censé se produire que lorsque la fonction a échoué, le
goto
cas pourrait être traité en plaçant unreturn
juste avant la première étiquette cible. Le code ci-dessus nécessiterait l'ajout d'unreturn
à la ligne marquée par*****
.Dans le scénario "nettoyage même dans le cas normal", je considérerais l'utilisation de
goto
comme étant plus claire que les constructionsdo
/while(0)
, entre autres parce que les étiquettes cibles elles-mêmes crient pratiquement "REGARDE-MOI" bien plus que les constructionsbreak
etdo
/while(0)
. Pour le cas "nettoyage uniquement en cas d'erreur", l'return
instruction finit par devoir être à peu près au pire endroit possible du point de vue de la lisibilité (les instructions de retour doivent généralement être soit au début d'une fonction, soit à quoi "ressemble" la fin); avoir unereturn
étiquette juste avant une cible répond à cette qualification beaucoup plus facilement que d'en avoir une juste avant la fin d'une «boucle».BTW, un scénario dans lequel j'utilise parfois
goto
pour la gestion des erreurs se trouve dans uneswitch
instruction, lorsque le code pour plusieurs cas partage le même code d'erreur. Même si mon compilateur serait souvent assez intelligent pour reconnaître que plusieurs cas se terminent par le même code, je pense qu'il est plus clair de dire:Bien que l'on puisse remplacer les
goto
instructions par{handle_error(); break;}
, et bien que l'on puisse utiliser une boucledo
/while(0)
aveccontinue
pour traiter le paquet d'exécution conditionnelle encapsulé, je ne pense pas vraiment que ce soit plus clair que d'utiliser ungoto
. De plus, bien qu'il soit possible de copier le code dePACKET_ERROR
partout où legoto PACKET_ERROR
est utilisé, et alors qu'un compilateur peut écrire le code dupliqué une fois et remplacer la plupart des occurrences par un saut vers cette copie partagée, l'utilisation degoto
facilite la détection des endroits qui place le paquet un peu différemment (par exemple si l'instruction "exécuter conditionnellement" décide de ne pas s'exécuter).la source
Personnellement, je suis un adepte du "Le pouvoir des dix - 10 règles pour rédiger un code critique de sécurité" .
Je vais inclure un petit extrait de ce texte qui illustre ce que je crois être une bonne idée à propos de goto.
Règle: Restreignez tout le code à des constructions de flux de contrôle très simples - n'utilisez pas d'instructions goto, de constructions setjmp ou longjmp et de récursivité directe ou indirecte.
Justification: un flux de contrôle plus simple se traduit par des capacités de vérification plus fortes et aboutit souvent à une meilleure clarté du code. Le bannissement de la récursivité est peut-être la plus grande surprise ici. Sans récursion, cependant, nous avons la garantie d'avoir un graphe d'appel de fonction acyclique, qui peut être exploité par les analyseurs de code, et peut directement aider à prouver que toutes les exécutions qui devraient être bornées sont en fait bornées. (Notez que cette règle n'exige pas que toutes les fonctions aient un point de retour unique - bien que cela simplifie souvent également le flux de contrôle. Il existe cependant suffisamment de cas où un retour d'erreur précoce est la solution la plus simple.)
Bannir l'utilisation de goto semble mauvais mais:
Si les règles semblent draconiennes au premier abord, gardez à l'esprit qu'elles sont destinées à permettre de vérifier le code là où très littéralement votre vie peut dépendre de son exactitude: code qui sert à contrôler l'avion sur lequel vous volez, la centrale nucléaire. à quelques kilomètres de chez vous, ou du vaisseau spatial qui transporte les astronautes en orbite. Les règles agissent comme la ceinture de sécurité de votre voiture: au début, elles sont peut-être un peu inconfortables, mais après un certain temps, leur utilisation devient une seconde nature et ne pas les utiliser devient inimaginable.
la source
goto
est d'utiliser un ensemble de booléens «intelligents» dans des if ou des boucles profondément imbriquées. Cela n'aide vraiment pas. Peut-être que vos outils le feront mieux, mais vous ne le ferez pas et vous êtes plus important.Je conviens que le nettoyage goto dans l'ordre inverse donné dans la question est le moyen le plus propre de nettoyer les choses dans la plupart des fonctions. Mais je voulais aussi souligner que parfois, vous voulez quand même que votre fonction nettoie. Dans ces cas, j'utilise la variante suivante if if (0) {label:} idiom pour aller au bon moment du processus de nettoyage:
la source
Il me semble que
cleanup_3
devrait faire son nettoyage, puis appelercleanup_2
. De même,cleanup_2
devrait faire son nettoyage, puis appeler cleanup_1. Il semble que chaque fois que vous le faitescleanup_[n]
, celacleanup_[n-1]
est nécessaire, donc cela devrait être la responsabilité de la méthode (de sorte que, par exemple,cleanup_3
ne puisse jamais être appelé sans appelercleanup_2
et éventuellement provoquer une fuite.)Compte tenu de cette approche, au lieu de gotos, vous appelleriez simplement la routine de nettoyage, puis reviendriez.
L'
goto
approche n'est ni fausse ni mauvaise , cependant, il convient de noter que ce n'est pas nécessairement l'approche la plus «propre» (IMHO).Si vous recherchez les performances optimales, je suppose que la
goto
solution est la meilleure. Je m'attends à ce qu'il ne soit pertinent, cependant, que dans quelques applications, critiques pour les performances (par exemple, les pilotes de périphériques, les périphériques intégrés, etc.). Sinon, c'est une micro-optimisation qui a une priorité inférieure à la clarté du code.la source
Je pense que la question ici est fallacieuse par rapport au code donné.
Considérer:
Par conséquent: do_something (), init_stuff () et prepare_stuff () devraient faire leur propre nettoyage . Avoir une fonction cleanup_1 () séparée qui nettoie après do_something () rompt la philosophie de l'encapsulation. C'est une mauvaise conception.
S'ils ont fait leur propre nettoyage, alors foo () devient assez simple.
D'autre part. Si foo () créait réellement son propre état qui devait être détruit, alors goto serait approprié.
la source
Voici ce que j'ai préféré:
la source
Ancienne discussion, cependant ... qu'en est-il de l'utilisation de "flèche anti-motif" et d'encapsuler plus tard chaque niveau imbriqué dans une fonction statique en ligne? Le code a l'air propre, il est optimal (lorsque les optimisations sont activées) et aucun goto n'est utilisé. En bref, divisez pour conquérir. Ci-dessous un exemple:
En termes d'espace, nous créons trois fois la variable dans la pile, ce qui n'est pas bon, mais cela disparaît lors de la compilation avec -O2 en supprimant la variable de la pile et en utilisant un registre dans cet exemple simple. Ce que j'ai obtenu du bloc ci-dessus
gcc -S -O2 test.c
était le suivant:la source
Je préfère utiliser la technique décrite dans l'exemple suivant ...
}
source: http://blog.staila.com/?p=114
la source
Nous utilisons la
Daynix CSteps
bibliothèque comme une autre solution pour le " problème goto " dans les fonctions init.Voir ici et ici .
la source
goto