106 votes

Valide l'utilisation de goto pour la gestion des erreurs en C?

Cette question est effectivement le résultat d'une discussion intéressante à programming.reddit.com il y a un moment. Il peut se résumer ainsi le 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 l' goto ici semble être la meilleure voie à suivre, résultant dans la plus propre et la plus efficace du code de toutes les possibilités, ou du moins il me semble. Citant Steve McConnell dans le Code Complet:

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

Un autre appui de cette approche vient du Linux Pilotes de Périphérique livre, dans cette section.

Qu'en pensez-vous? Cette affaire est d'une utilisation valide pour goto en C? Préférez-vous d'autres méthodes, qui produisent plus compliquées et/ou moins efficace du code, mais évitez goto?

78voto

Michael Burr Points 181287

FWIF, j'ai trouver l'erreur de manipulation de l'idiome que vous avez donné à la question de l'exemple pour être plus lisible et plus facile à comprendre que toutes les solutions de rechange fournis dans les réponses jusqu'à présent. Alors qu' goto est une mauvaise idée en général, il peut être utile pour le traitement des erreurs lorsqu'il est effectué dans un simple et uniforme. Dans cette situation, même si c'est un goto, il est utilisé dans bien définis et plus ou moins structurées.

20voto

Jonathan Leffler Points 299946

En règle générale, en évitant goto est une bonne idée, mais les abus qui ont été répandues lors de Dijkstra d'abord écrit "GOTO Considérées comme Nuisibles" ne même pas franchir la plupart des esprits comme une option de ces jours.

Ce que vous décrivez est une solution généralisable à l'erreur de traitement de problème, c'est bien avec moi aussi longtemps qu'elle est utilisée avec parcimonie.

Votre exemple peut être simplifiée de la façon suivante (é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;
}

La poursuite de ce 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, l'équivalent du code d'origine. Cela semble particulièrement propre, car le code d'origine est lui-même très propre et bien organisé. Souvent, les fragments de code ne sont pas aussi bien rangé que (si j'avais accepter un argument qu'ils devraient être); par exemple, il est souvent plus de l'état de passer à l'initialisation (programme d'installation) des routines que montré, et donc plus d'état à passer pour les routines de nettoyage trop.

18voto

psmears Points 7809

Je suis surpris que personne n'a proposé cette alternative, donc, même si la question a été autour d'un moment, je vais l'ajouter dans: une bonne façon de traiter ce problème est d'utiliser des variables pour garder une trace de l'état actuel. C'est une technique qui peut être utilisée si oui ou non goto est utilisé pour arriver au code de nettoyage. Comme toute technique de chiffrement, il a des avantages et des inconvénients, et ne pas être adapté à toutes les situations, mais si vous êtes de choisir un style, il est utile d'envisager, surtout si vous voulez éviter goto sans terminer profondément imbriqués ifs.

L'idée de base est que, pour chaque action de nettoyage qui doivent être prises, il est une variable dont la valeur nous permet de savoir si le nettoyage doit être fait ou pas.

Je vais vous montrer l' goto version tout d'abord, parce qu'il est plus proche du code dans 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 avantage de cette de plus certains de l'autre de ces techniques est que, si l'ordre de l'initialisation des fonctions est changé, le bon nettoyage arrivera encore - par exemple, à l'aide de l' switch méthode décrite dans une autre réponse, si l'ordre d'initialisation des modifications, puis l' switch doit être très soigneusement édité éviter d'essayer de nettoyer quelque chose n'a pas été initialisé en premier lieu.

Maintenant, certains pourraient faire valoir que cette méthode ajoute un tas de variables supplémentaires; en effet, dans ce cas, c'est vrai, mais dans la pratique souvent une variable existante déjà des pistes, ou peut être faite pour la piste, l'état requis. Par exemple, si l' prepare_stuff() est en fait un appel à l' malloc(), ou d' open(), alors la variable contenant le pointeur retourné ou descripteur de fichier peut être utilisé, par exemple:

int fd = -1;

....

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

...

cleanup:

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

Maintenant, si nous avons également suivre le statut de l'erreur avec une variable, nous pouvons éviter goto entièrement, et encore nettoyer correctement, sans avoir l'indentation qui devient plus profonde et plus profonde, plus d'initialisation nous avons besoin de:

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_initede = 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 est possible que les critiques de cette:

  • N'ont pas tous ces "si"s nuire à la performance? Non, car dans le cas de réussite, vous devez faire tous les contrôles de toute façon (sinon, vous n'êtes pas la vérification de tous les cas d'erreur); et dans le cas d'échec de la plupart des compilateurs optimiser la séquence de défaut if (oksofar) des contrôles à un seul saut vers le code de nettoyage (GCC n'est certainement) - et dans tous les cas, l'erreur de cas, elle est moins critique pour les performances.
  • N'est-ce pas l'ajout d'une autre variable? Dans ce cas, oui, mais souvent, l' return_value variable peut être utilisée pour jouer le rôle oksofar est de jouer ici. Si vous structurez vos fonctions pour retourner des erreurs de manière cohérente, vous pouvez même éviter le deuxième 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);
    }
    

    L'un des avantages du codage comme ça, c'est que la cohérence signifie que n'importe quel endroit où l'original programmeur a oublié de vérifier la valeur de retour colle dehors comme un pouce endolori, ce qui rend beaucoup plus facile à trouver (que l'on classe de bogues.

- C'est (encore) un style qui peut être utilisé pour résoudre ce problème. Utilisé correctement, il permet très propre, conforme au code, et, comme toute technique, dans de mauvaises mains il peut produire du code qui est de longue haleine et de confusion :-)

9voto

dirkgently Points 56879

Le problème avec l' goto mot-clé est la plupart du temps mal compris. Il n'est pas clair du mal. Vous avez juste besoin d'être conscient de l'extra chemins de contrôle que vous créez avec tous les goto. Il devient difficile de raisonner sur votre code, et donc de sa validité.

FWIW, si vous regardez en haut developer.apple.com tutoriels, ils prennent le goto approche de la gestion des erreurs.

Nous n'utilisons pas de gotos. Une grande importance est mise sur les valeurs de retour. La gestion des exceptions se fait via setjmp/longjmp -- quelle que soit la peu que vous le pouvez.

5voto

webmarc Points 101

Il n'y a rien de moralement condamnable sur l'instruction goto plus qu'il y a quelque chose de moralement condamnable (void)* les pointeurs.

Tout est dans la façon dont vous utilisez l'outil. Dans l' (trivial) cas où vous avez présenté, une instruction de cas peuvent atteindre la même logique, mais avec plus de surcharge. La vraie question est "quel est mon exigence de la vitesse?"

goto est tout simplement rapide, surtout si vous êtes prudent pour s'assurer qu'il compile pour un petit saut. Parfait pour les applications où la vitesse est une prime. Pour d'autres applications, il fait probablement de sens de prendre la surcharge frappé avec if/else + cas pour la maintenabilité.

Rappelez-vous: goto ne tue pas les applications, les développeurs de tuer des applications.

Mise à JOUR: 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 ; 
      } 
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X