web-dev-qa-db-fra.com

Utilisation valide de goto pour la gestion des erreurs en C?

Cette question est en fait le résultat d'une discussion intéressante à programming.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 semble ici être la meilleure solution, ce qui donne le code le plus propre et le plus efficace de toutes les possibilités, ou du moins me semble-t-il. Citer Steve McConnell dans Code Complete:

Le goto est utile dans une routine qui alloue des ressources, effectue opérations sur ces ressources, et puis désalloue les ressources. Avec un goto, vous pouvez nettoyer dans une section du code. Le goto réduit le probabilité de votre oubli à désallouer les ressources dans chaque lieu vous détectez une erreur.

Un autre support pour cette approche provient du livre Linux Device Drivers, dans cette section .

Qu'est-ce que tu penses? Ce cas est-il une utilisation valide de goto en C? Préféreriez-vous d'autres méthodes produisant un code plus compliqué et/ou moins efficace, tout en évitant goto?

84
Eli Bendersky

FWIF, j’estime 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 n’importe laquelle des solutions proposées jusqu’ici dans les réponses. Bien que goto soit une mauvaise idée en général, cela peut être utile pour le traitement des erreurs lorsqu'il est effectué de manière simple et uniforme. Dans cette situation, même s'il s'agit d'une goto, elle est utilisée de manière bien définie et plus ou moins structurée.

53
Michael Burr

En règle générale, il est judicieux d'éviter de se rendre à goto, mais les abus qui prévalaient lorsque Dijkstra écrivait pour la première fois «GOTO Considered Harmful» ne traversent même pas l'esprit de la plupart des gens, ces derniers temps.

Ce que vous décrivez est une solution généralisable au problème de gestion des erreurs - elle me convient si elle est utilisée avec précaution.

Votre exemple particulier peut être simplifié comme suit (étape 1):

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))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Continuer le processus:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

C'est, je crois, équivalent au code original. Cela semble particulièrement propre puisque le code d'origine était lui-même très propre et bien organisé. Souvent, les fragments de code ne sont pas aussi bien rangés que cela (bien que j'accepterais un argument selon lequel ils devraient l'être); Par exemple, il y a souvent plus d'état à passer aux routines d'initialisation (configuration) que ce qui est indiqué, et donc plus d'état à passer également aux routines de nettoyage.

16
Jonathan Leffler

Je suis surpris que personne n’ait suggéré cette alternative, alors même si la question existe depuis un moment, je l’ajouterai: un bon moyen de résoudre ce problème consiste à utiliser des variables pour suivre l’état actuel. C'est une technique qui peut être utilisée que goto soit utilisé ou non pour obtenir le code de nettoyage. Comme toute technique de codage, elle a des avantages et des inconvénients et ne convient pas à toutes les situations. Toutefois, si vous choisissez un style, il vaut la peine de la considérer, en particulier si vous souhaitez éviter goto sans vous retrouver avec des ifs profondément imbriqués.

L'idée de base est que, pour chaque opération de nettoyage à entreprendre, il existe une variable à partir de laquelle la valeur peut indiquer si le nettoyage doit être effectué ou non.

Je vais d'abord montrer la version goto, car elle se rapproche du code de la question d'origine.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Un des avantages de cette technique par rapport à d’autres est que, si l’ordre des fonctions d’initialisation est modifié, le nettoyage correct aura toujours lieu - par exemple, en utilisant la méthode switch décrite dans une autre réponse, si l’ordre d’initialisation change, le switch doit être édité avec précaution pour éviter d'essayer de nettoyer quelque chose qui n'a pas été initialisé.

Certains pourraient soutenir que cette méthode ajoute beaucoup de variables supplémentaires - et c'est vrai dans ce cas, mais c'est vrai - mais souvent une variable existante suit déjà, ou peut être amenée à suivre l'état requis. Par exemple, si la prepare_stuff() est en fait un appel à malloc() ou à open(), la variable contenant le pointeur ou le descripteur de fichier renvoyé peut être utilisée - par exemple:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

Maintenant, si nous suivons en outre le statut d'erreur avec une variable, nous pouvons éviter goto tout en nettoyant correctement, sans indentation de plus en plus profonde, plus l'initialisation dont nous avons besoin est importante:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Encore une fois, il y a des critiques potentielles à ce sujet:

  • Tous ces "si" ne font-ils pas mal à la performance? Non - car dans le cas de succès, vous devez quand même effectuer toutes les vérifications (sinon, vous ne vérifiez pas tous les cas d'erreur); et dans le cas de la défaillance, la plupart des compilateurs optimiseront la séquence des vérifications if (oksofar) défaillantes 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’a-t-il pas ajouté une autre variable? Dans ce cas, oui, mais la variable return_value peut souvent être utilisée pour jouer le rôle que oksofar joue ici. Si vous structurez vos fonctions pour renvoyer les erreurs de manière cohérente, vous pouvez même éviter le second if dans chaque cas:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }
    

    Un des avantages de ce type de codage est que la cohérence signifie que tout endroit où le programmeur original a oublié de vérifier la valeur de retour ressort comme un pouce endolori, ce qui facilite beaucoup la recherche de bogues.

Donc - c’est (encore) un autre style qui peut être utilisé pour résoudre ce problème. Utilisé correctement, il permet d'obtenir un code très propre et cohérent - et, comme toute technique, entre de mauvaises mains peut aboutir à un code long et déroutant :-)

15
psmears

Le problème avec le mot clé goto est généralement mal compris. Ce n'est pas purement diabolique. 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 tutoriels developer.Apple.com, ils adoptent l’approche goto de la gestion des erreurs. 

Nous n'utilisons pas de gotos. Une plus grande importance est accordée aux valeurs de retour. La gestion des exceptions se fait via setjmp/longjmp - le plus petit possible.

8
dirkgently

Il n’ya rien de plus moralement faux dans la déclaration de goto qu’il n’ya quelque chose de faux moralement avec des pointeurs (vides) *.

Tout est dans la façon dont vous utilisez l'outil. Dans le cas (trivial) que vous avez présenté, une déclaration de cas peut réaliser la même logique, avec toutefois davantage de temps système. La vraie question est: "quelle est ma vitesse requise?" 

goto est tout simplement rapide, surtout si vous veillez à ce que la compilation soit rapide. Parfait pour les applications où la vitesse est primordiale. Pour d'autres applications, il est probablement judicieux de prendre le surcoût avec if/else + pour la maintenabilité.

Rappelez-vous: goto ne tue pas les applications, les développeurs tuent les applications.

UPDATE: Voici l'exemple de cas

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 
4
webmarc

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 un seul goto vous permet de le faire facilement ... Cela peut vous faire gagner du temps.

2
toto

En général, j’aimerais considérer le fait qu’un morceau de code pourrait être écrit le plus clairement en utilisant goto en tant que 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 de goto tenterait de traiter le symptôme plutôt que la maladie. Votre exemple particulier pourrait ne pas être trop difficile à implémenter sans goto:

 faire {
 .. mettre en place chose1 qui aura besoin de nettoyage uniquement en cas de sortie anticipée 
 si (erreur) pause; 
 faire
 {
 .. mettre en place thing2 qui aura besoin de nettoyage en cas de sortie anticipée 
 si (erreur) pause; 
 // ***** VOIR LE TEXTE CONCERNANT CETTE LIGNE 
 } tandis que (0); 
 .. chose de nettoyage2; 
 } tandis que (0); 
 .. chose de nettoyage1; 

mais si le nettoyage n'était censé avoir lieu que lorsque la fonction a échoué, le cas goto peut être traité en plaçant un return juste avant le premier libellé cible. Le code ci-dessus nécessiterait l'ajout d'une return à la ligne marquée *****.

Dans le scénario "nettoyage, même dans les cas normaux", je considérerais que l'utilisation de goto est plus claire que les constructions do/while(0), entre autres parce que la cible elle-même crie pratiquement "LOOK AT ME" far moreso que les constructions break et do/while(0). Pour le cas "nettoyage uniquement en cas d'erreur", l'instruction return finit par se trouver à peu près au pire endroit possible du point de vue de la lisibilité (les instructions return doivent généralement se trouver soit au début d'une fonction, soit à ce à quoi "ressemble" " la fin); avoir une return juste avant qu'une étiquette cible réponde à cette qualification beaucoup plus facilement que d'en avoir une juste avant la fin d'une "boucle".

BTW, un scénario où j'utilise parfois goto pour la gestion des erreurs se trouve dans une instruction switch, lorsque le code de plusieurs cas partage le même code d'erreur. Bien que mon compilateur soit 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:

 REPARSE_PACKET: 
 commutateur (paquet [0]) 
 {
 case PKT_THIS_OPERATION: 
 si (condition problématique) 
 goto PACKET_ERROR; 
 ... gérer THIS_OPERATION 
 Pause;
 case PKT_THAT_OPERATION: 
 si (condition problématique) 
 goto PACKET_ERROR; 
 ... gérer That_OPERATION 
 Pause;
 ...
 case PKT_PROCESS_CONDITIONALLY 
 if (packet_length <9) 
 goto PACKET_ERROR; 
 if (condition_paquet impliquant un paquet [4]) 
 {
 packet_length - = 5; 
 memmove (packet, packet + 5, packet_length); 
 allez à REPARSE_PACKET; 
 } 
 autre
 {
 paquet [0] = PKT_CONDITION_SKIPPED; 
 packet [4] = packet_length; 
 packet_length = 5; 
 packet_status = READY_TO_SEND; 
 } 
 Pause;
 ...
 défaut:
 {
 PACKET_ERROR: 
 packet_error_count ++; 
 packet_length = 4; 
 paquet [0] = PKT_ERROR; 
 packet_status = READY_TO_SEND; 
 Pause;
 } 
 } 

Bien que l'on puisse remplacer les instructions goto par {handle_error(); break;} et que l'on puisse utiliser une boucle do/while(0) avec continue pour traiter le paquet conditionnel-execute encapsulé, je ne pense pas que ce soit plus clair que d'utiliser goto. En outre, s’il est possible de copier le code à partir de PACKET_ERROR partout où le goto PACKET_ERROR est utilisé, et qu’un compilateur peut écrire le code dupliqué une fois et remplacer la plupart des occurrences par un saut vers cette copie partagée, son utilisation de goto le facilite. remarquer des endroits qui configurent le paquet un peu différemment (par exemple, si l'instruction "exécuter conditionnellement" décide de ne pas exécuter).

1
supercat

Je conviens que le nettoyage 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 que votre fonction soit nettoyée de toute façon. Dans ces cas, j'utilise la variante suivante si if (0) {label:} idiome pour aller au bon moment du processus de nettoyage:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }
1
user1251840

Personnellement, je suis un adepte du "Le pouvoir de dix - 10 règles pour la rédaction de codes de sécurité critiques" . 

Je vais inclure un petit extrait de ce texte qui illustre ce que je pense être une bonne idée de la méthode goto.


Rule: limitez tout le code aux constructions de flux de contrôle très simples - n'utilisez pas d'instructions goto , De constructions setjmp ou longjmp, ni de récursion directe ou indirecte.

Justification: Un flux de contrôle plus simple se traduit par des capacités de vérification renforcées et entraîne souvent une amélioration de la clarté du code. Le bannissement de la récursion est peut-être la plus grande surprise ici. Sans récursion, cependant, nous avons la garantie d'avoir un graphe d'appel d'appel de fonction Acyclique, qui peut être exploité par des analyseurs de code, et peut aider À aider directement à prouver que toutes les exécutions devant être liées sont en fait liées . (Notez que cette règle ne nécessite pas que toutes les fonctions aient un point de retour unique – Bien que cela simplifie souvent aussi le flux de contrôle. Cependant, il existe suffisamment de cas... Où un retour anticipé est la solution la plus simple. )


Bannir l'utilisation de goto semble mauvais mais:

Si les règles semblent Draconiennes au début, gardez à l'esprit qu'elles sont conçues pour permettre de vérifier le code Dans les faits, votre vie peut dépendre de son exactitude: code utilisé pour contrôler l’avion sur lequel vous volez, la centrale nucléaire située à quelques kilomètres de votre lieu de résidence, ou le vaisseau spatial qui transporte les astronautes en orbite. Les règles agissent comme la ceinture de sécurité de votre voiture: initialement, elles sont peut-être un peu inconfortables, mais au bout d'un moment, leur utilisation devient une seconde nature et leur non utilisation devient inimaginable.

1
Trevor Boyd Smith

Voici ce que j'ai préféré:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}
0
Mikko Korkalo

Il me semble que cleanup_3 devrait effectuer son nettoyage, puis appelez cleanup_2. De même, cleanup_2 devrait effectuer son nettoyage, puis appelez cleanup_1. Il semble qu'à chaque fois que vous faites cleanup_[n], que cleanup_[n-1] soit requis, il devrait donc être de la responsabilité de la méthode (pour que, par exemple, cleanup_3 ne puisse jamais être appelé sans appeler cleanup_2 et éventuellement provoquer une fuite.)

Avec cette approche, au lieu de gotos, vous appelez simplement la routine de nettoyage, puis revenez.

L’approche goto n’est pas fausse ou mauvaise , mais il est juste de noter que ce n’est pas nécessairement l’approche la plus «propre» (IMHO).

Si vous recherchez des performances optimales, je suppose que la solution goto est la meilleure. Je ne m'attends toutefois à ce qu'il soit pertinent que dans certaines applications critiques (par exemple, les pilotes de périphérique, les périphériques intégrés, etc.). Sinon, il s'agit d'une micro-optimisation moins prioritaire que la clarté du code.

0
Ryan Emerle

Je pense que la question ici est fallacieuse en ce qui concerne le code donné.

Considérer:

  1. do_something (), init_stuff () et prepare_stuff () semblent savoir s'ils ont échoué, car ils renvoient false ou nil dans ce cas.
  2. La responsabilité de la création de l'état semble être de la responsabilité de ces fonctions, puisqu'un état n'est pas créé directement dans foo ().

Par conséquent: do_something (), init_stuff () et prepare_stuff () devraient être faire leur propre nettoyage . Avoir une fonction cleanup_1 () distincte qui nettoie après que do_something () rompt la philosophie de l'encapsulation. C'est mauvais design.

S'ils ont fait leur propre nettoyage, alors foo () devient assez simple.

D'autre part. Si foo () crée effectivement son propre état qui doit être détruit, alors goto serait approprié.

0
Simon Woodside