Est-ce un cas d'utilisation décent pour goto en C?

59

J'hésite vraiment à poser cette question, car je ne souhaite pas "solliciter un débat, des arguments, des sondages ou des discussions prolongées", mais je suis novice en langage C et souhaite mieux comprendre les modèles courants utilisés dans le langage.

J'ai récemment entendu du dégoût pour la gotocommande, mais j'ai aussi récemment trouvé un cas d'utilisation décent pour cette commande.

Code comme ceci:

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

Si la partie nettoyage est très similaire, vous pourriez écrire un peu plus joli (mon avis?) Comme ceci:

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

Est-ce un cas d'utilisation courant ou acceptable gotoen C? Existe-t-il un moyen différent / meilleur de le faire?

Robz
la source
3
Voir aussi cette question (et ma réponse ).
Keith Thompson
2
Plus qu'un enrobage au sucre, le compilateur garantit le nettoyage et la vérification des erreurs, alors qu'en C, vous devez le faire manuellement, en violant DRY et en garantissant que vous êtes trop paresseux pour vérifier le code d'erreur ou que vous vous êtes trompé d'étiquette.
DeadMG
3
@Izkata: Non, le but est d'avoir des fonctions (comme des destructeurs) qui sont appelées implicitement par le compilateur lorsqu'une exception est levée.
DeadMG
6
@DeadMG: Utiliser une autre langue n'est pas toujours une option.
Benjamin Kloster
13
Je pense qu'avec un libellé approprié, l'exemple peut paraître assez décent:goto hell;
Gnat le

Réponses:

48

L' gotoinstruction (et ses étiquettes correspondantes) est une primitive de contrôle de flux (ainsi que l'exécution conditionnelle d'une instruction). Je veux dire par là qu’ils sont là pour vous permettre de construire des réseaux de contrôle de flux de programmes. Vous pouvez les considérer comme une modélisation des flèches entre les nœuds d'un diagramme.

Certaines d'entre elles peuvent être optimisées immédiatement, lorsqu'il existe un flux linéaire direct (vous utilisez simplement une séquence d'instructions de base). Il est préférable de remplacer les autres modèles par des constructions de programmation structurées lorsque celles-ci sont disponibles; si cela ressemble à une whileboucle, utilisez une whileboucle , OK? Les modèles de programmation structurés sont certainement au moins potentiellement plus clairs d'intention qu'un désordre d' gotoénoncés.

Cependant, C n'inclut pas toutes les constructions de programmation structurées possibles. (Je ne vois pas encore si toutes les informations pertinentes ont été découvertes; le rythme de découverte est lent, mais j’hésite à dire que toutes ont été retrouvées.) Parmi ceux que nous connaissons, C manque clairement du try/ catch/ finallystructure (et exceptions aussi). Il lui manque également une breakboucle multi-niveaux . C'est le genre de choses que l' gotoon peut utiliser pour mettre en œuvre. Il est possible d’utiliser d’autres systèmes pour les réaliser également - nous savons que C possède un ensemble suffisant degotoles primitives - mais elles impliquent souvent la création de variables de drapeau et de conditions de boucle ou de garde beaucoup plus complexes; augmenter l'enchevêtrement de l'analyse de contrôle avec l'analyse des données rend le programme plus difficile à comprendre en général. Cela rend également plus difficile l’optimisation du compilateur et l’exécution rapide de la CPU (la plupart des constructions de contrôle de flux - et certainement goto - sont très économiques).

Ainsi, bien que vous ne devriez pas utiliser gotosauf besoin, vous devez être conscient du fait qu'il existe et qu'il peut être nécessaire, et si vous en avez besoin, vous ne devriez pas vous sentir trop mal. Un exemple de cas où cela est nécessaire est la désallocation de ressource lorsqu'une fonction appelée renvoie une condition d'erreur. (C'est-à-dire, try/ finally.) Il est possible d'écrire cela sans cela, gotomais cela peut avoir des inconvénients, tels que des problèmes de maintenance. Un exemple de l'affaire:

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

Le code pourrait être encore plus court, mais c'est suffisant pour démontrer le point.

Donal Fellows
la source
4
+1: En C, techniquement, le goto n'est jamais "nécessaire" - il y a toujours un moyen de le faire, ça devient compliqué ..... Pour un ensemble de directives robustes concernant l'utilisation de MISRA C
mattnz
1
Préférez - vous try/catch/finallyà gotomalgré la même, encore plus envahissante (car il peut se propager à travers plusieurs fonctions / modules) forme de code spaghetti qui est possible à l' aide try/catch/finally?
Autiste
65

Oui.

Il est utilisé, par exemple, dans le noyau Linux. Voici un email de la fin d'un fil d'il y a presque une décennie , en gras, le mien:

De: Robert Love
Objet: Re: une chance de 2.6.0-test *?
Date: 12 janvier 2003 17:58:06 -0500

Le Dim, 2003-01-12 à 17:22, Rob Wilkens a écrit:

Je dis "s'il vous plaît ne pas utiliser goto" et plutôt une fonction "cleanup_lock" et ajoute que, avant toutes les déclarations de retour .. Cela ne devrait pas être un fardeau. Oui, il demande au développeur de travailler un peu plus fort, mais le résultat final est un meilleur code.

Non, c'est brut et ça gonfle le noyau . Il insère un tas d’ordures pour N chemins d’erreur, par opposition au code de sortie une fois à la fin. L'empreinte du cache est la clé et vous venez de la tuer.

Ce n'est pas plus facile à lire.

En guise de dernier argument, cela ne nous permet pas de faire proprement le vent et le déroulement habituels de la pile , c’est-à-dire

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

Maintenant arrête ça.

Robert Love

Cela dit, il faut beaucoup de discipline pour éviter de créer du code spaghetti une fois que vous vous êtes habitué à utiliser goto. Par conséquent, à moins d'écrire quelque chose qui nécessite de la vitesse et une faible empreinte mémoire (comme un noyau ou un système intégré), vous devriez vraiment Pensez-y avant d'écrire le premier goto.

Izkata
la source
21
Notez qu'un noyau est différent d'un programme non noyau en ce qui concerne la priorité sur la vitesse brute par rapport à la lisibilité. En d' autres termes, ils ont DÉJÀ profilé et a constaté que ils ont besoin pour optimiser leur code pour la vitesse avec goto.
11
Utiliser stack und-wind pour gérer le nettoyage par erreur sans pousser réellement sur la pile! C'est une utilisation géniale de goto.
mike30
1
@ user1249, Rubbish, vous ne pouvez pas profiler chaque application {passée, existante ou future} qui utilise un morceau de code {library, kernel}. Vous devez simplement être rapide.
Pacerier
1
Non lié: Je suis étonné de voir comment les gens peuvent utiliser les listes de diffusion pour accomplir quoi que ce soit, sans parler de projets aussi gigantesques. C'est tellement ... primitif. Comment les gens entrent-ils avec la caserne de messages?!
Alexandre
2
Je ne suis pas tellement préoccupé par la modération. Si quelqu'un est assez doux pour se faire détourner par un abruti sur Internet, votre projet s'en trouvera probablement mieux sans eux. Je m'inquiète davantage de l'impossibilité pratique de suivre le flot de messages entrants et de la manière dont vous pourriez avoir une discussion en arrière-plan naturelle avec si peu d'outils pour garder une trace des citations, par exemple.
Alexandre
14

À mon avis, le code que vous avez publié est un exemple d'utilisation valide goto, car vous sautez uniquement vers le bas et vous ne l'utilisez que comme un gestionnaire d'exception primitif.

Cependant , à cause du vieux débat sur le goto, les programmeurs évitent gotodepuis environ 40 ans et ne sont donc pas habitués à lire du code avec goto. Ceci est une raison valable pour éviter d'aller à: c'est tout simplement pas la norme.

J'aurais réécrit le code de manière plus lisible par les programmeurs C:

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

Avantages de cette conception:

  • La fonction effectuant le travail réel n'a pas besoin de s'occuper de tâches qui ne sont pas pertinentes pour son algorithme, telles que l'allocation de données.
  • Le code semblera moins étranger aux programmeurs C, car ils ont peur de goto et des labels.
  • Vous pouvez centraliser la gestion des erreurs et la désallocation au même endroit, en dehors de la fonction exécutant l'algorithme. Cela n'a pas de sens pour une fonction de gérer ses propres résultats.
Doc Brown
la source
11

Un document célèbre décrivant des cas d’utilisation valide de était Programmation structurée avec déclaration GOTO de Donald E. Knuth (Université de Stanford). Le document a été publié à une époque où l'utilisation de GOTO était considérée comme un péché par certains et lorsque le mouvement de la programmation structurée était à son apogée. Vous voudrez peut-être jeter un coup d'œil à GoTo Considered Harmful.

Aucune chance
la source
9

En Java, vous le feriez comme ceci:

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

Je l'utilise beaucoup. Bien que je n'aime pas gototrop, dans la plupart des autres langages de style C, j'utilise votre code; il n'y a pas d'autre moyen de le faire. (Sauter hors des boucles imbriquées est un cas similaire; en Java, j'utilise un libellé breaket partout ailleurs, un goto.)

RalphChapin
la source
3
Aw c'est une structure de contrôle soignée.
Bryan Boettcher
4
C'est vraiment intéressant. Je penserais normalement à utiliser la structure try / catch / finally pour cela en java (lancer des exceptions au lieu de casser).
Robz
5
C'est vraiment illisible (du moins pour moi). Si présent, les exceptions sont bien meilleures.
m3th0dman
1
@ m3th0dman Je suis d'accord avec vous dans cet exemple particulier (traitement des erreurs). Mais il existe d'autres cas (non exceptionnels) où cet idiome pourrait être utile.
Konrad Rudolph
1
Les exceptions sont coûteuses, elles doivent générer une erreur, une trace de pile et beaucoup plus de courrier indésirable. Cette rupture d'étiquette donne une sortie propre d'une boucle de vérification. À moins que vous ne vous souciez de la mémoire et de la vitesse, utilisez une exception.
Tschallacka
8

Je pense que c'est un cas d'utilisation correcte, mais dans le cas où « erreur » est rien de plus qu'une valeur booléenne, il y a une autre façon d'accomplir ce que vous voulez:

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

Ceci utilise l'évaluation de court-circuit d'opérateurs booléens. Si ce "meilleur" dépend de vos goûts personnels et de votre habitude à cet idiome.

Doc Brown
la source
1
Le problème avec ceci est que sa errorvaleur pourrait perdre tout son sens avec tous les OR.
James
@James: modifié ma réponse à cause de votre commentaire
Doc Brown
1
Ce n'est pas suffisant. Si une erreur s’est produite lors de la première fonction, je ne souhaite pas exécuter la deuxième ou la troisième fonction.
Robz
2
Si avec sténographie évaluation que vous voulez dire court-circuit d' évaluation, c'est exactement pas ce qui fait ici en raison de l'utilisation de ou au lieu de bitwise logique OU.
Chris dit Réintégrer Monica le
1
@ChristianRau: merci, modifié ma réponse en conséquence
Doc Brown
6

Le guide de style linux donne des raisons spécifiques d'utilisation gotoqui correspondent à votre exemple:

https://www.kernel.org/doc/Documentation/process/coding-style.rst

La raison d'utiliser gotos est:

  • les déclarations inconditionnelles sont plus faciles à comprendre et à suivre
  • la nidification est réduite
  • les erreurs en ne mettant pas à jour les points de sortie individuels lors de modifications sont empêchées
  • enregistre le travail du compilateur pour optimiser le code redondant;)

Déni de responsabilité Je ne suis pas supposé partager mon travail. Les exemples ici sont un peu artificiels alors supportez s'il vous plaît avec moi.

C'est bon pour la gestion de la mémoire. J'ai récemment travaillé sur du code qui avait alloué de la mémoire de manière dynamique (par exemple, un char *rendu par une fonction). Une fonction qui examine un chemin et vérifie si le chemin est valide en analysant les jetons du chemin:

tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;

Pour moi, le code suivant est beaucoup plus agréable et facile à maintenir si vous devez ajouter un varNplus1:

int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;

Maintenant, le code avait toutes sortes d'autres problèmes, à savoir que N se situait quelque part au-dessus de 10 et que sa fonction comptait plus de 450 lignes, avec 10 niveaux d'imbrication à certains endroits.

Mais j’ai proposé à mon superviseur de le refactoriser, ce que j’ai fait et c’est maintenant un tas de fonctions qui sont toutes courtes et qui ont toutes le style linux.

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}

Si on considère l'équivalent sans gotos:

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}

Pour moi, dans le premier cas, il est évident que si la première fonction revient NULL, nous sommes sortis d’ici et nous revenons 0. Dans le second cas, je dois faire défiler l'écran vers le bas pour voir que le if contient la fonction entière. Accordé le premier me l'indique stylistiquement (le nom " out") et le second le fait syntaxiquement. Le premier est encore plus évident.

De plus, je préfère grandement avoir des free()déclarations à la fin d'une fonction. C'est en partie parce que, selon mon expérience, les free()déclarations au milieu de fonctions ont une odeur nauséabonde et m'indiquent que je devrais créer un sous-programme. Dans ce cas, j'ai créé var1dans ma fonction et ne pouvais pas le free()faire dans un sous-programme, mais c'est pourquoi le goto out_freestyle, goto out est si pratique.

Je pense que les programmeurs doivent être éduqués en croyant que gotoc'est mal. Ensuite, quand ils seront suffisamment matures, ils devront parcourir le code source de Linux et lire le guide de style de Linux.

J'ajouterais que j'utilise ce style de manière très cohérente, chaque fonction ayant un int retval, un out_freelabel et un label out. En raison de la cohérence stylistique, la lisibilité est améliorée.

Bonus: Pause et continue

Disons que vous avez une boucle while

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}

Il y a d'autres problèmes avec ce code, mais une chose est l'instruction continue. Je voudrais réécrire le tout, mais on m'a demandé de le modifier légèrement. Cela m'aurait pris des jours pour le reformuler de manière à me satisfaire, mais le changement réel a duré environ une demi-journée de travail. Le problème est que même si nous " continue, nous devons toujours libérer var1et var2. Je devais ajouter a var3, et cela me donnait envie de vomir pour refléter les déclarations free ().

J'étais un stagiaire relativement nouveau à l'époque, mais je m'intéressais depuis longtemps au code source de Linux, alors j'ai demandé à mon superviseur si je pouvais utiliser une déclaration goto. Il a dit oui et j'ai fait ceci:

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}

Je pense que continuer est au mieux acceptable, mais pour moi, c'est comme un goto avec une étiquette invisible. La même chose vaut pour les pauses. Je préférerais toujours continuer ou interrompre sauf si, comme c'était le cas ici, cela vous oblige à refléter les modifications apportées à plusieurs endroits.

Et je devrais également ajouter que cette utilisation de goto next;et l' next:étiquette ne me satisfont pas. Ils sont simplement meilleurs que de refléter le free()'et les count++déclarations.

gotoont presque toujours tort, mais il faut savoir quand ils sont bons à utiliser.

Une chose dont je n’ai pas parlé est la gestion des erreurs qui a été couverte par d’autres réponses.

Performance

On peut regarder la mise en oeuvre de strtok () http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c

#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}

S'il vous plaît, corrigez-moi si je me trompe, mais je pense que l' cont:étiquette et la goto cont;déclaration sont là pour la performance (ils ne rendent sûrement pas le code plus lisible). Ils pourraient être remplacés par du code lisible en faisant

while( isDelim(*s++,delim));

pour ignorer les délimiteurs. Mais pour être aussi rapide que possible et éviter les appels de fonctions inutiles, ils le font de cette façon.

J'ai lu le journal de Dijkstra et je le trouve assez ésotérique.

google "déclaration de dijkstra goto considérée comme nuisible" parce que je n'ai pas assez de réputation pour publier plus de 2 liens.

Je l'ai vu cité comme une raison de ne pas utiliser de goto et sa lecture n'a rien changé en ce qui concerne mes utilisations de goto.

Addenda :

Je suis venu avec une règle bien en pensant à tout cela continue et se casse.

  • Si, dans une boucle while, vous avez une action continue, le corps de la boucle while doit être une fonction et l'instruction continue doit être une instruction return.
  • Si, dans une boucle while, vous avez une instruction break, alors la boucle while elle-même doit être une fonction et la rupture doit devenir une instruction return.
  • Si vous avez les deux, alors quelque chose ne va pas.

Ce n'est pas toujours possible en raison de problèmes de portée, mais j'ai constaté que cela facilite beaucoup la tâche de raisonnement à propos de mon code. J'avais remarqué que chaque fois qu'une boucle tenait une pause ou continuait, je ressentais un mauvais pressentiment.

Philippe Carphin
la source
2
+1 mais puis-je être en désaccord sur un point? "Je pense que les programmeurs doivent être éduqués en croyant que les goto sont diaboliques." Peut-être bien, mais j’ai d’abord appris à programmer en BASIC, avec des numéros de ligne et des GOTO, sans éditeur de texte, en 1975. J’ai rencontré la programmation structurée dix ans plus tard. toute pression pour arrêter. Aujourd'hui, j'utilise GOTO plusieurs fois par an pour diverses raisons, mais cela ne se présente pas souvent. Ne pas avoir été élevé pour croire que GOTO est un mal ne m'a fait aucun mal que je sache, et il aurait même pu faire du bien. Ce n'est que moi.
thb
1
Je pense que vous avez raison à ce sujet. J'avais été élevé avec l'idée que les GOTO ne devaient pas être utilisés et, par hasard, je parcourais le code source de Linux à une époque où je travaillais sur du code comportant ces fonctions avec plusieurs points de sortie avec mémoire à libérer. Sinon, je n'aurais jamais entendu parler de ces techniques.
Philippe Carphin
1
@thb En outre, histoire amusante, j’ai demandé à mon supérieur hiérarchique à l’époque, en tant que stagiaire, la permission d’utiliser les GOTO et j’ai veillé à ce que je lui explique que j’allais les utiliser de la même manière que dans le Le noyau Linux et il a dit "Ok, c'est logique, et aussi, je ne savais pas que vous pouviez utiliser les GOTO en C".
Philippe Carphin
1
@thb Je ne sais pas s'il est bon de se lancer dans des boucles (au lieu de les casser) comme celui-ci ? Eh bien, c’est un mauvais exemple, mais j’aperçois que le quicksort avec les instructions goto (exemple 7a) de la programmation structurée de Knuth avec go to Statements n’est pas très compréhensible.
Yai0Phah
@ Yai0Phah Je vais expliquer mon point mais mon explication ne diminue pas votre bel exemple 7a! J'approuve l'exemple. Pourtant, les étudiants en deuxième année qui aiment la profession aiment parler aux gens de goto. Il est difficile de trouver une utilisation pratique de goto depuis 1985 qui cause des problèmes considérables, alors que l'on peut trouver des gotos inoffensifs qui facilitent le travail du programmeur. Quoi qu'il en soit, la programmation moderne présente si rarement que, lorsque cela se produit, mon conseil est que, si vous voulez l'utiliser, vous devriez probablement simplement l'utiliser. Goto va bien. Le principal problème de goto est que certains pensent que déprécier goto les rend intelligents.
thb
5

Personnellement, je le refrais plus comme ça:

int DoLotsOfStuffThatCouldFail (paramstruct *params)
{
    int errcode = EC_NOERROR;

    if ((errcode = FunctionThatCouldFail1 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail2 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail3 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail4 (params)) != EC_NOERROR) return errcode;

    return EC_NOERROR;
}

void DoStuff (paramstruct *params)
{
    int errcode = EC_NOERROR;

    InitStuffThatMayNeedToBeCleaned (params);

    if ((errcode = DoLotsOfStuffThatCouldFail (params)) != EC_NOERROR)
    {
         CleanupAfterError (params, errcode);
    }
}

Cela serait plus motivé par l’évitement de l’imbrication profonde que par celui obtenu par goto (IMO, un problème encore plus grave avec le premier exemple de code), et serait bien sûr dépendant du fait que CleanupAfterError soit hors de portée (dans ce cas, "params" pourrait soit une structure contenant de la mémoire allouée que vous devez libérer, un FICHIER * que vous devez fermer ou autre chose).

Un avantage majeur de cette approche réside dans le fait qu’il est à la fois plus simple et plus propre d’intégrer un futur pas supplémentaire hypothétique entre, par exemple, FTCF2 et FTCF3 (ou de supprimer une étape en cours existante), de sorte qu’il se prête mieux à la maintenabilité (et à la personne hérite de mon code ne voulant pas me lyncher!) - mis à part, la version imbriquée ne le manque pas.

Maximus Minimus
la source
1
Je n’ai pas dit cela dans ma question, mais il est possible que les FTCF n’aient PAS les mêmes paramètres, ce qui complique un peu plus ce schéma. Merci quand même.
Robz
3

Jetez un coup d’œil aux directives de codage C de la MISRA (Association pour la fiabilité des logiciels dans l’industrie automobile) qui permettent à goto de répondre à des critères stricts (auxquels votre exemple correspond

Là où je travaille, le même code serait écrit - pas besoin de rien - Eviter les débats religieux inutiles à leur sujet est un avantage considérable pour toute entreprise de logiciels.

error = function_that_could_fail_1();
if(!error) {
  error = function_that_could_fail_2();
}
if(!error) {
  error = function_that_could_fail_3();
} 
if(!error) {
...
if (error) {
  cleanup:
} 

ou pour "goto in drag" - quelque chose d'encore plus douteux que goto, mais qui contourne le "No goto ever !!!" camp) "C’est sûr que ça doit aller, on n’utilise pas Goto" ....

do {
  if (error = function_that_could_fail_1() ){
    break 
  }
  if (error = function_that_could_fail_2() ){
    break 
  }
  ....... 
} while (0) 
cleanup();
.... 

Si les fonctions ont le même type de paramètre, mettez-les dans une table et utilisez une boucle -

Mattnz
la source
2
Les directives actuelles de MISRA-C: 2004 ne permettent la goto sous aucune forme (voir règle 14.4). Notez que le comité MISRA a toujours été confus à ce sujet, ils ne savent pas sur quel pied se tenir. Premièrement, ils ont interdit inconditionnellement l’utilisation de goto, continue, etc. Mais dans le brouillon de la prochaine MISRA 2011, ils veulent les autoriser à nouveau. En passant, veuillez noter que la MISRA interdit la cession dans les instructions if, pour de très bonnes raisons, car elle est beaucoup plus dangereuse que toute utilisation de goto.
1
Du point de vue analytique, l' ajout d' un drapeau à un programme équivaut à dupliquer tout le code où le drapeau est portée, ayant tous if(flag)en un seul exemplaire prendre le « si » branche et ayant chacune des déclarations correspondantes dans l'autre copie prendre la " autre". Les actions qui définissent et effacent l'indicateur sont en réalité des "gotos" qui basculent entre ces deux versions du code. Il y a des moments où l'utilisation de drapeaux est plus propre que toute autre solution, mais l'ajout d'un drapeau pour sauvegarder une gotocible ne constitue pas un bon compromis.
Supercat
1

J'utilise également gotosi le do/while/continue/breakhackery alternatif serait moins lisible.

gotos ont l’avantage que leurs cibles ont un nom et qu’elles lisent goto something;. Cela peut être plus lisible que breakou continuesi vous n'arrêtez ni ne continuez quelque chose.

aib
la source
4
N'importe où à l'intérieur d'une do ... while(0)construction ou d' une autre, qui n'est pas une boucle réelle mais une tentative effrénée d'empêcher l'utilisation de a goto.
aib
1
Ah, merci, je ne connaissais pas cette marque particulière de "Pourquoi diable voudrait-on que quelqu'un fasse ça ?!" construit encore.
Benjamin Kloster
2
Habituellement, le hackery do / while / continue / break ne devient illisible que lorsque le module qui le contient est beaucoup trop long en premier lieu.
John R. Strohm
2
Je ne trouve rien dans ceci comme une justification pour utiliser goto. Pause et continuer ont une conséquence évidente. goto ... où? Où est l'étiquette? Break et goto vous dire exactement où la prochaine étape est et sa proximité.
Rig
1
L'étiquette doit bien sûr être visible de l'intérieur de la boucle. Je suis d'accord avec le commentaire de @John R. Strohm sur la longueur. Et votre point, traduit par boucle de hackery, devient "Sortir de quoi? Ce n'est pas une boucle!". En tout cas, cela devient ce que le PO craignait, alors j'abandonne la discussion.
aib
-1
for (int y=0; y<height; ++y) {
    for (int x=0; x<width; ++x) {
        if (find(x, y)) goto found;
    }
}
found:
aib
la source
S'il n'y a qu'une boucle, breakfonctionne exactement comme goto, bien que ne portant pas de stigmatisation.
9000 le
6
-1: D'abord, x et y sont HORS PORTÉE à found :, donc cela ne vous aide pas. Deuxièmement, avec le code tel que rédigé, le fait que vous soyez arrivé à trouvé: ne signifie pas que vous avez trouvé ce que vous cherchiez.
John R. Strohm
C’est parce que c’est le plus petit exemple auquel je puisse penser pour le cas de rupture d’un grand nombre de boucles. S'il vous plaît n'hésitez pas à le modifier pour une meilleure étiquette ou un contrôle effectué.
aib
1
Mais gardez également à l’esprit que les fonctions C ne sont pas nécessairement exemptes d’effets secondaires.
aib
1
@ JohnR.Strohm Cela n'a pas beaucoup de sens ... Le libellé 'found' est utilisé pour casser la boucle, pas pour vérifier les variables. Si je voulais vérifier les variables, je pourrais faire quelque chose comme ceci: for (int y = 0; y <hauteur; ++ y) {pour (int x = 0; x <largeur; ++ x) {if (find ( x, y)) {doSomeThingWith (x, y); goto trouvé; }}} trouvé:
YoYoYonnY
-1

Il y aura toujours des camps qui disent qu'un moyen est acceptable et un autre qui ne l'est pas. Les entreprises pour lesquelles j'ai travaillé ont froncé les sourcils ou fortement découragé leur utilisation. Personnellement, je ne peux penser à aucun moment où j'en ai utilisé un, mais cela ne signifie pas qu'ils sont mauvais , mais une autre façon de faire les choses.

En C, je fais généralement ce qui suit:

  • Recherchez des conditions susceptibles d'empêcher le traitement (entrées incorrectes, etc.) et "renvoyer"
  • Effectuez toutes les étapes nécessitant une allocation de ressources (par exemple, des mallocs)
  • Effectuer le traitement, où plusieurs étapes vérifient le succès
  • Libérer toutes les ressources, si elles ont été allouées avec succès
  • Renvoyer les résultats

Pour le traitement, en utilisant votre exemple goto, je ferais ceci:

error = function_that_could_fail_1 (); if (! error) {error = function_that_could_fail_2 (); } if (! error) {error = function_that_could_fail_3 (); }

Il n'y a pas d'imbrication, et à l'intérieur des clauses if, vous pouvez créer un rapport d'erreur si l'étape générée génère une erreur. Donc, cela ne doit pas forcément être "pire" qu'une méthode utilisant gotos.

Je n'ai pas encore rencontré de cas où quelqu'un a un goto qui ne peut pas être fait avec une autre méthode et qui est tout aussi lisible / compréhensible et qui est la clé, IMHO.

pcm
la source