web-dev-qa-db-fra.com

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

J'hésite vraiment à poser cette question, parce que je ne veux pas "solliciter un débat, des arguments, un sondage ou une discussion approfondie", mais je suis nouveau à C et je veux mieux comprendre les modèles courants utilisés dans le langage.

J'ai récemment entendu un peu de dégoût pour la commande goto, mais j'ai également récemment trouvé un cas d'utilisation décent pour elle.

Code comme celui-ci:

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, pourrait être écrit 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 de goto en C? Existe-t-il une manière différente/meilleure de procéder?

60
Robz

L'instruction goto (et ses étiquettes correspondantes) sont un contrôle de flux primitif (avec exécution conditionnelle d'une instruction). Par cela, je veux dire qu'ils sont là pour vous permettre de construire des réseaux de contrôle de flux de programme. Vous pouvez les considérer comme une modélisation des flèches entre les nœuds d'un organigramme.

Certains d'entre eux peuvent être optimisés immédiatement, là où il y a un flux linéaire direct (vous utilisez simplement une séquence d'instructions de base). Il est préférable de remplacer d'autres modèles par des constructions de programmation structurées lorsque celles-ci sont disponibles; si elle ressemble à une boucle while, tilisez une boucle while, OK? Les modèles de programmation structurés sont certainement au moins potentiellement plus clairs d'intention qu'un désordre d'instructions goto.

Pourtant, C n'inclut pas toutes les constructions de programmation structurées possibles. (Il n'est pas clair pour moi que toutes les découvertes pertinentes ont été découvertes encore; le taux de découverte est lent maintenant, mais j'hésiterais à dire que toutes ont été trouvées.) De celles que nous connaissons, C n'a certainement pas le try/catch/finally structure (et exceptions aussi). Il manque également break- from-loop à plusieurs niveaux. Ce sont les types de choses qu'un goto peut être utilisé pour implémenter. Il est possible d'utiliser d'autres schémas pour les faire aussi - nous savons que C a un suffisant ensemble de primitives non_goto - mais celles-ci impliquent souvent la création de variables d'indicateur et d'une boucle beaucoup plus complexe ou conditions de garde; l'augmentation de l'enchevêtrement de l'analyse de contrôle avec l'analyse des données rend le programme plus difficile à comprendre globalement. Cela rend également plus difficile l'optimisation du compilateur et l'exécution rapide du CPU (la plupart des constructions de contrôle de flux - et certainementgoto - sont très bon marché).

Ainsi, même si vous ne devez pas utiliser goto sauf si vous en avez besoin, vous devez être conscient qu'il existe et qu'il peut être nécessaire, et si vous en avez besoin, vous ne devriez pas vous sentir trop mauvais. Un exemple de cas où il est nécessaire est la désallocation de ressources lorsqu'une fonction appelée renvoie une condition d'erreur. (C'est-à-dire try/finally.) Il est possible d'écrire cela sans goto mais cela peut avoir ses propres inconvénients, comme les problèmes de maintenance. Un exemple du cas:

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.

48
Donal Fellows

Oui.

Il est utilisé, par exemple, dans le noyau Linux. Voici un e-mail de la fin d'un fil d'il y a près d'une décennie , le mien audacieux:

De: Robert Love
Objet: Objet: possibilité de test 2.6.0 *?
Date: 12 janvier 2003 17:58:06 -0500

Le dimanche 12 janvier 2003 à 17 h 22, Rob Wilkens a écrit:

Je dis "s'il vous plaît ne pas utiliser goto" et à la place avoir une fonction "cleanup_lock" et ajouter cela avant toutes les instructions de retour .. Cela ne devrait pas être un fardeau. Oui, cela demande au développeur de travailler un peu plus dur, mais le résultat final est un meilleur code.

Non, c'est grossier et ça gonfle le noyau . Il insère un tas d'ordure pour N chemins d'erreur, au lieu d'avoir le 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.

Comme dernier argument, , il 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 vous empêcher de créer du code spaghetti une fois que vous vous êtes habitué à utiliser goto, donc à moins que vous n'écriviez 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 vous écrivez le premier goto.

67
Izkata

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

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

J'aurais réécrit le code comme quelque chose de plus facile à lire 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 qui effectue le travail proprement dit n'a pas besoin de se préoccuper des tâches qui ne sont pas pertinentes pour son algorithme, comme l'allocation de données.
  • Le code semblera moins étranger aux programmeurs C, car ils ont peur du goto et des étiquettes.
  • Vous pouvez centraliser la gestion des erreurs et la désallocation au même endroit, en dehors de la fonction faisant l'algorithme. Il n'est pas logique qu'une fonction gère ses propres résultats.
14
user29079

Un article célèbre décrivant un cas d'utilisation valide de était Programmation structurée avec déclaration GOTO par Donald E. Knuth (Stanford University). Le journal est apparu à l'époque où l'utilisation de GOTO était considérée comme un péché par certains et où le mouvement pour la programmation structurée était à son apogée. Vous voudrez peut-être jeter un oeil à GoTo Considéré Nocif.

11
NoChance

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

J'utilise beaucoup ça. Tout comme je n'aime pas les goto, dans la plupart des autres langages de style C, j'utilise votre code; il n'y a pas d'autre bon moyen de le faire. (Sauter des boucles imbriquées est un cas similaire; en Java j'utilise un break étiqueté et partout ailleurs j'utilise un goto.))

9
RalphChapin

Je pense que est un cas d'utilisation décent, mais au cas où "erreur" n'est rien de plus qu'une valeur booléenne, il existe une manière différente 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
}

Cela utilise l'évaluation des courts-circuits des opérateurs booléens. Si cela "mieux", c'est à votre goût personnel et comment vous êtes habitué à cet idiome.

8
Doc Brown

Le guide de style linux donne des raisons spécifiques d'utiliser les goto qui correspondent à votre exemple:

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

La justification de l'utilisation de gotos est la suivante:

  • les déclarations inconditionnelles sont plus faciles à comprendre et à suivre
  • l'emboîtement est réduit
  • erreurs en ne mettant pas à jour les points de sortie individuels lorsque les modifications sont empêchées
  • enregistre le travail du compilateur pour optimiser le code redondant;)

Clause de non-responsabilité Je ne suis pas censé partager mon travail. Les exemples ici sont un peu artificiels, alors soyez indulgents avec moi.

C'est bon pour la gestion de la mémoire. J'ai récemment travaillé sur du code ayant une mémoire allouée dynamiquement (par exemple un char * Renvoyé 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;

Maintenant, pour moi, le code suivant est beaucoup plus agréable et plus facile à gérer 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 était quelque part au-dessus de 10, et la fonction était sur 450 lignes, avec 10 niveaux d'imbrication à certains endroits.

Mais j'ai proposé à mon superviseur de le refactoriser, ce que j'ai fait et maintenant c'est un tas de fonctions qui sont toutes courtes, et elles 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 l'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 pour moi que si la première fonction retourne NULL, nous sommes sortis d'ici et nous retournons 0. Dans le second cas, je dois faire défiler vers le bas pour voir que le if contient la fonction entière. Certes, le premier m'indique cela 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 instructions free() à la fin d'une fonction. C'est en partie parce que, d'après mon expérience, les instructions free() au milieu des fonctions sentent mauvais et m'indiquent que je devrais créer un sous-programme. Dans ce cas, j'ai créé var1 Dans ma fonction et ne pouvais pas free() dans un sous-programme, mais c'est pourquoi le style goto out_free, Goto out est si pratique.

Je pense que les programmeurs doivent être élevés en croyant que les goto sont mauvais. Ensuite, lorsqu'ils sont suffisamment matures, ils doivent parcourir le code source Linux et lire le guide de style linux.

Je dois ajouter que j'utilise ce style de manière très cohérente, chaque fonction a une étiquette int retval, une étiquette out_free Et une étiquette out. En raison de la cohérence stylistique, la lisibilité est améliorée.

Bonus: casse et continue

Dites 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 choses qui ne vont pas avec ce code, mais une chose est l'instruction continue. Je voudrais réécrire le tout, mais j'ai été chargé de le modifier légèrement. Il m'aurait fallu des jours pour le refaçonner d'une manière qui me satisfasse, mais le changement réel était d'environ une demi-journée de travail. Le problème est que même si nous 'continue' nous avons encore besoin de libérer var1 Et var2. J'ai dû ajouter un var3, Et cela m'a donné envie de vomir d'avoir à refléter les instructions free ().

J'étais un stagiaire relativement nouveau à l'époque, mais j'avais regardé le code source Linux pour le plaisir il y a quelque temps, j'ai donc demandé à mon superviseur si je pouvais utiliser une instruction goto. Il a dit oui, et je l'ai fait:

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 ça va au mieux, mais pour moi, c'est comme un goto avec une étiquette invisible. Il en va de même pour les pauses. Je préférerais quand même continuer ou interrompre à moins que, comme c'était le cas ici, cela ne vous oblige à refléter les modifications à plusieurs endroits.

Et je dois également ajouter que cette utilisation de goto next; Et de l'étiquette next: Ne me satisfait pas. Ils sont simplement meilleurs que la mise en miroir des instructions free() et count++.

goto sont presque toujours faux, mais il faut savoir quand ils sont bons à utiliser.

Une chose que je n'ai pas discutée est la gestion des erreurs qui a été couverte par d'autres réponses.

Performances

On peut regarder l'implémentation 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 */
}

Veuillez me corriger si je me trompe, mais je crois que l'étiquette cont: Et l'instruction goto cont; Sont là pour la performance (elles ne rendent certainement 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 fonction inutiles, ils le font de cette façon.

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

google "déclaration dijkstra goto considérée comme nuisible" car je n'ai pas assez de réputation pour poster 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é dans la mesure où mes utilisations de goto sont concérées.

Addendum :

Je suis venu avec une règle soignée tout en pensant à tout cela à propos de continue et de rupture.

  • Si dans une boucle while, vous avez une continuation, le corps de la boucle while doit être une fonction et la continuation 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, il se peut que quelque chose cloche.

Ce n'est pas toujours possible en raison de problèmes de portée, mais j'ai trouvé que cela rendait beaucoup plus facile de raisonner sur mon code. J'avais remarqué que chaque fois qu'une boucle while avait une pause ou une continuation, cela me donnait une mauvaise impression.

7
Philippe Carphin

Personnellement, je le remanierais plus comme ceci:

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é en évitant l'imbrication profonde qu'en évitant le goto cependant (IMO un problème pire avec le premier exemple de code), et dépendrait bien sûr de CleanupAfterError étant possible hors de portée (dans ce cas, "params" pourrait être une structure contenant de la mémoire allouée que vous devez libérer, un FICHIER * que vous devez fermer ou autre).

Un avantage majeur que je vois avec cette approche est qu'il est à la fois plus facile et plus propre de placer une hypothétique étape supplémentaire future entre, disons, FTCF2 et FTCF3 (ou de supprimer une étape actuelle existante), de sorte qu'elle se prête mieux à la maintenabilité (et à la personne qui hérite de mon code ne voulant pas me lyncher!) - allez-y, la version imbriquée manque.

5
Maximus Minimus

Jetez un coup d'œil aux directives de codage C MISRA (Motor Industry Software Reliability Association) qui permettent de passer à des critères stricts (auxquels votre exemple répond)

Là où je travaille, le même code serait écrit - pas besoin - Éviter les débats religieux inutiles à leur sujet est un gros plus dans n'importe quelle maison 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 contourne le "No goto Ever !!!" camp) "Sûrement ça doit être OK, 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 -

3
mattnz

J'utilise également goto si l'alternative do/while/continue/break le piratage serait moins lisible.

gotos ont l'avantage que leurs cibles ont un nom et lisent goto something;. Cela peut être plus lisible que break ou continue si vous n'arrêtez pas ou ne continuez pas quelque chose.

1
aib