Est-il toujours avantageux d'utiliser « goto » dans un langage qui prend en charge les boucles et les fonctions ?Si oui, pourquoi?

StackOverflow https://stackoverflow.com/questions/24451

Question

J'ai longtemps eu l'impression que goto ne devrait jamais être utilisé si possible.En parcourant libavcodec (qui est écrit en C) l'autre jour, j'en ai remarqué plusieurs utilisations.Est-il toujours avantageux d'utiliser goto dans un langage qui prend en charge les boucles et les fonctions ?Si oui, pourquoi?

Était-ce utile?

La solution

Il y a plusieurs raisons d'utiliser l'instruction "goto" que je connais (certains en ont déjà parlé) :

Quitter proprement une fonction

Souvent, dans une fonction, vous pouvez allouer des ressources et devoir quitter à plusieurs endroits.Les programmeurs peuvent simplifier leur code en plaçant le code de nettoyage des ressources à la fin de la fonction, et tous les "points de sortie" de la fonction iront sur l'étiquette de nettoyage.De cette façon, vous n’avez pas besoin d’écrire du code de nettoyage à chaque « point de sortie » de la fonction.

Quitter les boucles imbriquées

Si vous êtes dans une boucle imbriquée et que vous devez en sortir tous boucles, un goto peut rendre cela beaucoup plus propre et plus simple que les instructions break et les vérifications if.

Améliorations des performances de bas niveau

Ceci n'est valable que dans le code critique en termes de performances, mais les instructions goto s'exécutent très rapidement et peuvent vous donner un coup de pouce lorsque vous parcourez une fonction.Il s’agit cependant d’une arme à double tranchant, car un compilateur ne peut généralement pas optimiser le code contenant des gotos.

Notez que dans tous ces exemples, les gotos sont limités à la portée d'une seule fonction.

Autres conseils

Tous ceux qui sont anti-goto cite, directement ou indirectement, l'ouvrage d'Edsger Dijkstra GoTo considéré comme nocif article pour étayer leur position.Dommage que l'article de Dijkstra ait pratiquement rien à voir avec la manière goto les déclarations sont utilisées de nos jours et donc ce que dit l'article a peu ou pas d'applicabilité à la scène de programmation moderne.Le goto-less meme se rapproche désormais d'une religion, jusqu'à ses écritures dictées d'en haut, ses grands prêtres et le rejet (ou pire) des hérétiques perçus.

Replaçons l'article de Dijkstra dans son contexte pour faire la lumière sur le sujet.

Lorsque Dijkstra écrivait son article, les langages populaires de l'époque étaient des langages procéduraux non structurés comme le BASIC, le FORTRAN (les dialectes antérieurs) et divers langages d'assemblage.Il était assez courant que les personnes utilisant des langages de niveau supérieur sautent partout dans leur base de code en fils d'exécution tordus et contorsionnés qui ont donné naissance au terme "code spaghetti".Vous pouvez le voir en vous rendant sur le jeu Trek classique écrit par Mike Mayfield et essayant de comprendre comment les choses fonctionnent.Prenez quelques instants pour y réfléchir.

CE C'est « l'utilisation effrénée de la déclaration go to » contre laquelle Dijkstra s'insurgeait dans son article en 1968. CE C'est l'environnement dans lequel il a vécu qui l'a amené à rédiger cet article.La possibilité de sauter n'importe où dans votre code à tout moment, c'est ce qu'il critiquait et exigeait qu'il soit arrêté.En comparant cela aux pouvoirs anémiques de goto en C ou dans d'autres langages plus modernes est tout simplement ridicule.

J'entends déjà les chants élevés des cultistes face à l'hérétique."Mais", scanderont-ils, "vous pouvez rendre le code très difficile à lire avec goto en C." Oh ouais ?Vous pouvez rendre le code très difficile à lire sans goto aussi.Comme celui-ci:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

Pas un goto en vue, donc ça doit être facile à lire, non ?Ou que diriez-vous de celui-ci :

a[900];     b;c;d=1     ;e=1;f;     g;h;O;      main(k,
l)char*     *l;{g=      atoi(*      ++l);       for(k=
0;k*k<      g;b=k       ++>>1)      ;for(h=     0;h*h<=
g;++h);     --h;c=(     (h+=g>h     *(h+1))     -1)>>1;
while(d     <=g){       ++O;for     (f=0;f<     O&&d<=g
;++f)a[     b<<5|c]     =d++,b+=    e;for(      f=0;f<O
&&d<=g;     ++f)a[b     <<5|c]=     d++,c+=     e;e= -e
;}for(c     =0;c<h;     ++c){       for(b=0     ;b<k;++
b){if(b     <k/2)a[     b<<5|c]     ^=a[(k      -(b+1))
<<5|c]^=    a[b<<5      |c]^=a[     (k-(b+1     ))<<5|c]
;printf(    a[b<<5|c    ]?"%-4d"    :"    "     ,a[b<<5
|c]);}      putchar(    '\n');}}    /*Mike      Laman*/

Non goto là non plus.Il doit donc être lisible.

Quel est mon point avec ces exemples?Ce ne sont pas les fonctionnalités du langage qui rendent le code illisible et impossible à maintenir.Ce n'est pas la syntaxe qui le fait.Ce sont de mauvais programmeurs qui sont à l'origine de cela.Et les mauvais programmeurs, comme vous pouvez le voir dans l'élément ci-dessus, peuvent faire n'importe lequel caractéristique linguistique illisible et inutilisable.Comme le for des boucles là-haut.(Vous pouvez les voir, n'est-ce pas ?)

Pour être honnête, certaines constructions linguistiques sont plus faciles à abuser que d’autres.Cependant, si vous êtes un programmeur C, j'examinerais de beaucoup plus près environ 50 % des utilisations de #define bien avant de partir en croisade contre goto!

Ainsi, pour ceux qui ont pris la peine de lire jusqu’ici, il y a plusieurs points clés à noter.

  1. L'article de Dijkstra sur goto instructions ont été écrites pour un environnement de programmation où goto était un parcelleplus potentiellement dommageable que dans la plupart des langages modernes qui ne sont pas un assembleur.
  2. Jeter automatiquement toutes les utilisations de goto À cause de cela, c'est aussi rationnel que de dire "J'ai essayé de m'amuser une fois mais je n'ai pas aimé alors maintenant je suis contre".
  3. Il existe des usages légitimes du moderne (anémique) goto instructions dans le code qui ne peuvent pas être adéquatement remplacées par d'autres constructions.
  4. Il existe bien entendu des utilisations illégitimes des mêmes déclarations.
  5. Il existe également des utilisations illégitimes des déclarations de contrôle modernes comme le " godo" abomination où un toujours faux do la boucle est interrompue lors de l'utilisation break à la place d'un goto.C’est souvent pire qu’une utilisation judicieuse des goto.

Obéir aveuglément aux meilleures pratiques n’est pas une bonne pratique.L'idée d'éviter goto La principale forme de contrôle de flux consiste à éviter de produire du code spaghetti illisible.Utilisés avec parcimonie et aux bons endroits, ils peuvent parfois constituer le moyen le plus simple et le plus clair d’exprimer une idée.Walter Bright, le créateur du compilateur Zortech C++ et du langage de programmation D, les utilise fréquemment, mais judicieusement.Même avec le goto déclarations, son code est toujours parfaitement lisible.

Conclusion :Éviter goto pour éviter goto est inutile.Ce que vous voulez vraiment éviter, c'est de produire du code illisible.Si ton goto-le code chargé est lisible, alors il n'y a rien de mal à cela.

Depuis goto rend le raisonnement sur le déroulement du programme difficile1 (alias."code spaghetti"), goto n'est généralement utilisé que pour compenser les fonctionnalités manquantes :L'utilisation de goto peut en fait être acceptable, mais seulement si le langage ne propose pas de variante plus structurée pour atteindre le même objectif.Prenons l'exemple de Doute :

La règle avec goto que nous utilisons est que goto est acceptable pour passer à un seul point de nettoyage de sortie dans une fonction.

C'est vrai, mais seulement si le langage ne permet pas la gestion structurée des exceptions avec du code de nettoyage (tel que RAII ou finally), qui fait mieux le même travail (car il est spécialement conçu pour le faire), ou lorsqu'il y a une bonne raison de ne pas utiliser la gestion structurée des exceptions (mais vous n'aurez jamais ce cas sauf à un niveau très bas).

Dans la plupart des autres langues, la seule utilisation acceptable de goto est de quitter les boucles imbriquées.Et même là, il est presque toujours préférable d’intégrer la boucle externe dans sa propre méthode et de l’utiliser. return plutôt.

Autre que ça, goto est le signe que l'on n'a pas suffisamment réfléchi à un morceau de code en particulier.


1 Langues modernes qui prennent en charge goto mettre en œuvre certaines restrictions (par ex. goto ne peut pas accéder ou sortir des fonctions), mais le problème reste fondamentalement le même.

Soit dit en passant, il en va bien entendu de même pour d’autres fonctionnalités linguistiques, notamment les exceptions.Et il existe généralement des règles strictes en place pour utiliser ces fonctionnalités uniquement lorsque cela est indiqué, comme la règle de ne pas utiliser d'exceptions pour contrôler le flux de programme non exceptionnel.

Eh bien, il y a une chose qui est toujours pire que goto's;utilisation étrange d'autres opérateurs de flux de programme pour éviter un goto :

Exemples:

    // 1
    try{
      ...
      throw NoErrorException;
      ...
    } catch (const NoErrorException& noe){
      // This is the worst
    } 


    // 2
    do {
      ...break; 
      ...break;
    } while (false);


    // 3
    for(int i = 0;...) { 
      bool restartOuter = false;
      for (int j = 0;...) {
        if (...)
          restartOuter = true;
      if (restartOuter) {
        i = -1;
      }
    }

etc
etc

Dans C# changer déclaration ne permet pas de chute.Donc aller à est utilisé pour transférer le contrôle à une étiquette de boîtier de commutation spécifique ou au défaut étiquette.

Par exemple:

switch(value)
{
  case 0:
    Console.Writeln("In case 0");
    goto case 1;
  case 1:
    Console.Writeln("In case 1");
    goto case 2;
  case 2:
    Console.Writeln("In case 2");
    goto default;
  default:
    Console.Writeln("In default");
    break;
}

Modifier:Il existe une exception à la règle de « pas de chute ».La transition est autorisée si une instruction case ne contient pas de code.

#ifdef TONGUE_IN_CHEEK

Perl a un goto qui vous permet de mettre en œuvre des appels de queue du pauvre.:-P

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

Ok, donc ça n'a rien à voir avec les C goto.Plus sérieusement, je suis d'accord avec les autres commentaires sur l'utilisation goto pour le nettoyage ou pour la mise en œuvre L'appareil de Duff, ou semblable.Il s’agit d’utiliser, pas d’abuser.

(Le même commentaire peut s'appliquer à longjmp, des exceptions, call/cc, etc. --- ils ont des utilisations légitimes, mais peuvent facilement faire l'objet d'abus.Par exemple, lancer une exception uniquement pour échapper à une structure de contrôle profondément imbriquée, dans des circonstances totalement non exceptionnelles.)

J'ai écrit plus de quelques lignes de langage assembleur au fil des ans.En fin de compte, chaque langage de haut niveau se compile en gotos.D'accord, appelez-les « branches » ou « sauts » ou autre chose, mais ce sont des incontournables.Quelqu'un peut-il écrire un assembleur sans goto ?

Maintenant, bien sûr, vous pouvez faire remarquer à un programmeur Fortran, C ou BASIC que se déchaîner avec gotos est une recette de spaghetti bolognaise.La réponse n’est cependant pas de les éviter, mais de les utiliser avec précaution.

Un couteau peut être utilisé pour préparer à manger, libérer quelqu’un ou tuer quelqu’un.Devons-nous nous passer des couteaux par peur de ces derniers ?De même, le goto :utilisé avec négligence, il gêne, utilisé avec précaution, il aide.

Jeter un coup d'œil à Quand utiliser Goto lors de la programmation en C:

Bien que l'utilisation de goto soit presque toujours une mauvaise pratique de programmation (vous pouvez sûrement trouver une meilleure façon de faire XYZ), il y a des moments où ce n'est vraiment pas un mauvais choix.Certains diront même que, lorsque cela est utile, c'est le meilleur choix.

La plupart de ce que j'ai à dire sur goto ne s'applique qu'à C.Si vous utilisez C++, il n'y a aucune bonne raison d'utiliser goto à la place des exceptions.En C, cependant, vous n'avez pas la puissance d'un mécanisme de gestion des exceptions, donc si vous souhaitez séparer la gestion des erreurs du reste de la logique de votre programme et éviter de réécrire le code de nettoyage plusieurs fois dans votre code, alors goto peut être un bon choix.

Qu'est ce que je veux dire?Vous pourriez avoir un code qui ressemble à ceci :

int big_function()
{
    /* do some work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* clean up*/
    return [success];
}

C'est très bien jusqu'à ce que vous réalisiez que vous devez modifier votre code de nettoyage.Ensuite, vous devez passer par et effectuer 4 modifications.Maintenant, vous pourriez décider que vous pouvez simplement encapsuler tout le nettoyage dans une seule fonction ;ce n'est pas une mauvaise idée.Mais cela signifie que vous devrez faire attention avec les pointeurs - si vous envisagez de libérer un pointeur dans votre fonction de nettoyage, il n'y a aucun moyen de le définir pour qu'il pointe ensuite vers NULL à moins que vous ne transmettiez un pointeur vers un pointeur.Dans de nombreux cas, vous n’utiliserez plus ce pointeur de toute façon, ce n’est donc peut-être pas un problème majeur.D'un autre côté, si vous ajoutez un nouveau pointeur, un descripteur de fichier ou tout autre élément nécessitant un nettoyage, vous devrez alors modifier à nouveau votre fonction de nettoyage ;et ensuite vous devrez modifier les arguments de cette fonction.

En utilisant goto, ce sera

int big_function()
{
    int ret_val = [success];
    /* do some work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
end:
    /* clean up*/
    return ret_val;
}

L'avantage ici est que votre code après la fin a accès à tout ce dont il aura besoin pour effectuer le nettoyage, et vous avez réussi à réduire considérablement le nombre de points de modification.Un autre avantage est que vous êtes passé de plusieurs points de sortie pour votre fonction à un seul ;il n'y a aucune chance que vous reveniez accidentellement de la fonction sans nettoyer.

De plus, puisque goto n'est utilisé que pour accéder à un seul point, ce n'est pas comme si vous créiez une masse de code spaghetti sautant d'avant en arrière pour tenter de simuler des appels de fonction.Au contraire, goto aide réellement à écrire du code plus structuré.


En un mot, goto doit toujours être utilisé avec parcimonie et en dernier recours – mais il y a un moment et un lieu pour cela.La question ne devrait pas être « est-ce que vous devez l’utiliser » mais « est-ce le meilleur choix » pour l’utiliser.

L'une des raisons pour lesquelles goto est mauvais, outre le style de codage, est que vous pouvez l'utiliser pour créer chevauchement, mais non imbriqué boucles:

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

Cela créerait une structure de flux de contrôle bizarre, mais peut-être légale, où une séquence comme (a, b, c, b, a, b, a, b, ...) est possible, ce qui rendrait les pirates du compilateur mécontents.Apparemment, il existe un certain nombre d'astuces d'optimisation intelligentes qui reposent sur l'absence de ce type de structure.(Je devrais vérifier ma copie du livre du dragon...) Le résultat pourrait (en utilisant certains compilateurs) être que d'autres optimisations ne sont pas effectuées pour le code qui contient gotos.

Cela pourrait être utile si vous savoir cela arrive juste, "oh, au fait", à persuader le compilateur d'émettre du code plus rapide.Personnellement, je préférerais essayer d'expliquer au compilateur ce qui est probable et ce qui ne l'est pas avant d'utiliser une astuce comme goto, mais sans doute, je pourrais aussi essayer goto avant de pirater l'assembleur.

Je trouve drôle que certaines personnes aillent jusqu'à donner une liste de cas où goto est acceptable, en disant que toutes les autres utilisations sont inacceptables.Pensez-vous vraiment connaître tous les cas où goto est le meilleur choix pour exprimer un algorithme ?

Pour illustrer, je vais vous donner un exemple que personne ici n'a encore montré :

Aujourd'hui, j'écrivais du code pour insérer un élément dans une table de hachage.La table de hachage est un cache de calculs précédents qui peuvent être écrasés à volonté (affectant les performances mais pas l'exactitude).

Chaque compartiment de la table de hachage dispose de 4 emplacements et j'ai un certain nombre de critères pour décider quel élément écraser lorsqu'un compartiment est plein.À l'heure actuelle, cela signifie effectuer jusqu'à trois passages dans un seau, comme ceci :

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

Maintenant, si je n'utilisais pas goto, à quoi ressemblerait ce code ?

Quelque chose comme ça:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

Cela semblerait de pire en pire si d'autres passes étaient ajoutées, tandis que la version avec goto conserve le même niveau d'indentation à tout moment et évite l'utilisation d'instructions if parasites dont le résultat est impliqué par l'exécution de la boucle précédente.

Il existe donc un autre cas où goto rend le code plus propre et plus facile à écrire et à comprendre...Je suis sûr qu'il y en a beaucoup d'autres, alors ne prétendez pas connaître tous les cas où goto est utile, en rejetant les bons auxquels vous ne pouviez pas penser.

La règle avec goto que nous utilisons est que goto est acceptable pour passer à un seul point de nettoyage de sortie dans une fonction.Dans les fonctions vraiment complexes, nous assouplissons cette règle pour permettre d’autres avancées.Dans les deux cas, nous évitons les instructions if profondément imbriquées qui se produisent souvent avec la vérification des codes d'erreur, ce qui facilite la lisibilité et la maintenance.

La discussion la plus réfléchie et la plus approfondie sur les instructions goto, leurs utilisations légitimes et les constructions alternatives qui peuvent être utilisées à la place des « instructions goto vertueuses » mais qui peuvent être utilisées à mauvais escient aussi facilement que les instructions goto, est l'article de Donald Knuth : «Programmation structurée avec des instructions goto", dans Computing Surveys de décembre 1974 (volume 6, no.4.pp.261 à 301).

Il n’est pas surprenant que certains aspects de ce document vieux de 39 ans soient datés :Des augmentations de plusieurs ordres de grandeur dans la puissance de traitement rendent certaines améliorations de performances de Knuth imperceptibles pour des problèmes de taille moyenne, et de nouvelles constructions de langage de programmation ont été inventées depuis lors.(Par exemple, les blocs try-catch englobent le Construct de Zahn, bien qu'ils soient rarement utilisés de cette manière.) Mais Knuth couvre tous les aspects de l'argument et devrait être obligatoirement lu avant que quiconque ne ressasse le problème une fois de plus.

Dans un module Perl, vous souhaitez occasionnellement créer des sous-programmes ou des fermetures à la volée.Le fait est qu'une fois que vous avez créé le sous-programme, comment y accéder.Vous pouvez simplement l'appeler, mais si le sous-programme utilise caller() cela ne sera pas aussi utile qu'il pourrait l'être.C'est là que le goto &subroutine la variation peut être utile.

Voici un exemple rapide :

sub AUTOLOAD{
  my($self) = @_;
  my $name = $AUTOLOAD;
  $name =~ s/.*:://;

  *{$name} = my($sub) = sub{
    # the body of the closure
  }

  goto $sub;

  # nothing after the goto will ever be executed.
}

Vous pouvez également utiliser cette forme de goto pour fournir une forme rudimentaire d’optimisation des appels finals.

sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;

  $tally *= $n--;
  @_ = ($n,$tally);
  goto &factorial;
}

( Dans Perl5 version 16 ce serait mieux écrit comme goto __SUB__; )

Il existe un module qui importera un tail modificateur et celui qui importera recur si vous n'aimez pas utiliser cette forme de goto.

use Sub::Call::Tail;
sub AUTOLOAD {
  ...
  tail &$sub( @_ );
}

use Sub::Call::Recur;
sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;
  recur( $n-1, $tally * $n );
}

La plupart des autres raisons d'utiliser goto sont mieux réalisés avec d'autres mots-clés.

Comme redoje mets un peu de code :

LABEL: ;
...
goto LABEL if $x;
{
  ...
  redo if $x;
}

Ou aller au last d'un peu de code provenant de plusieurs endroits :

goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
  last if $x;
  ...
  last if $y
  ...
}

Si oui, pourquoi?

C n'a pas de rupture multi-niveaux/étiquetée, et tous les flux de contrôle ne peuvent pas être facilement modélisés avec les primitives d'itération et de décision de C.gotos contribue grandement à corriger ces défauts.

Parfois, il est plus clair d'utiliser une variable flag pour effectuer une sorte de rupture pseudo-multi-niveaux, mais ce n'est pas toujours supérieur au goto (au moins un goto permet de déterminer facilement où va le contrôle, contrairement à une variable flag ), et parfois vous ne voulez tout simplement pas payer le prix des performances des drapeaux/autres contorsions pour éviter le goto.

libavcodec est un morceau de code sensible aux performances.L'expression directe du flux de contrôle est probablement une priorité, car elle aura tendance à mieux fonctionner.

Tout aussi bien, personne n'a jamais mis en œuvre l'instruction "COME FROM"....

Je trouve l'utilisation de do{} while(false) tout à fait révoltante.Il est concevable que cela puisse me convaincre que cela est nécessaire dans certains cas étranges, mais jamais qu'il s'agit d'un code propre et sensé.

Si vous devez effectuer une telle boucle, pourquoi ne pas rendre explicite la dépendance à l'égard de la variable flag ?

for (stepfailed=0 ; ! stepfailed ; /*empty*/)

Le GOTO peut bien sûr être utilisé, mais il y a une chose plus importante que le style du code, ou si le code est ou non lisible, que vous devez garder à l'esprit lorsque vous l'utilisez : le code à l'intérieur n'est peut-être pas aussi robuste que vous le pensez.

Par exemple, regardez les deux extraits de code suivants :

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

Un code équivalent avec GOTO

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

La première chose à laquelle nous pensons est que le résultat des deux bits de code sera cette « Valeur de A :0" (on suppose une exécution sans parallélisme, bien sûr)

Ce n'est pas exact :dans le premier échantillon, A sera toujours 0, mais dans le deuxième échantillon (avec l'instruction GOTO) A pourrait ne pas être 0.Pourquoi?

La raison est que depuis un autre point du programme je peux insérer un GOTO FINAL sans contrôler la valeur de A.

Cet exemple est très évident, mais à mesure que les programmes deviennent plus complexes, la difficulté de voir ce genre de choses augmente.

Des informations connexes peuvent être trouvées dans le célèbre article de M.Dijkstra "Une affaire contre la déclaration GO TO"

1) L'utilisation la plus courante de goto que je connaisse consiste à émuler la gestion des exceptions dans des langages qui ne la proposent pas, notamment en C.(Le code donné par Nuclear ci-dessus n'est que cela.) Regardez le code source de Linux et vous verrez un milliard de gotos utilisés de cette façon ;il y avait environ 100 000 gotos dans le code Linux selon une enquête rapide menée en 2013 : http://blog.regehr.org/archives/894.L'utilisation de Goto est même mentionnée dans le guide de style de codage Linux : https://www.kernel.org/doc/Documentation/CodingStyle.Tout comme la programmation orientée objet est émulée à l'aide de structures remplies de pointeurs de fonction, goto a sa place dans la programmation C.Alors qui a raison :Dijkstra ou Linus (et tous les codeurs du noyau Linux) ?C'est théorie contre.pratique en gros.

Il existe cependant le piège habituel lié au fait de ne pas avoir de support au niveau du compilateur et de vérifier les constructions/modèles courants :il est plus facile de les utiliser mal et d'introduire des bogues sans vérifications au moment de la compilation.Windows et Visual C++ mais en mode C offrent une gestion des exceptions via SEH/VEH pour cette raison :les exceptions sont utiles même en dehors des langages POO, c'est-à-diredans un langage procédural.Mais le compilateur ne peut pas toujours sauvegarder votre bacon, même s'il offre un support syntaxique pour les exceptions dans le langage.Prenons comme exemple de ce dernier cas le fameux bug "goto fail" d'Apple SSL, qui vient de dupliquer un goto avec des conséquences désastreuses (https://www.imperialviolet.org/2014/02/22/applebug.html):

if (something())
  goto fail;
  goto fail; // copypasta bug
printf("Never reached\n");
fail:
  // control jumps here

Vous pouvez avoir exactement le même bug en utilisant des exceptions prises en charge par le compilateur, par ex.en C++ :

struct Fail {};

try {
  if (something())
    throw Fail();
    throw Fail(); // copypasta bug
  printf("Never reached\n");
}
catch (Fail&) {
  // control jumps here
}

Mais les deux variantes du bug peuvent être évitées si le compilateur analyse et vous avertit du code inaccessible.Par exemple, la compilation avec Visual C++ au niveau d'avertissement /W4 détecte le bogue dans les deux cas.Java, par exemple, interdit le code inaccessible (là où il peut le trouver !) pour une très bonne raison :il s'agit probablement d'un bug dans le code de Joe moyen.Tant que la construction goto n'autorise pas les cibles que le compilateur ne peut pas facilement comprendre, comme les gotos vers les adresses calculées (**), il n'est pas plus difficile pour le compilateur de trouver du code inaccessible dans une fonction avec gotos que d'utiliser Dijkstra. -code approuvé.

(**) Note de bas de page:Les accès aux numéros de ligne calculés sont possibles dans certaines versions de Basic, par ex.GOTO 10*x où x est une variable.De manière plutôt déroutante, en Fortran, « goto calculé » fait référence à une construction équivalente à une instruction switch en C.La norme C n'autorise pas les gotos calculés dans le langage, mais uniquement les gotos déclarés statiquement/syntaxiquement.GNU C possède cependant une extension pour obtenir l'adresse d'une étiquette (l'opérateur unaire, préfixe &&) et permet également d'accéder à une variable de type void*.Voir https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html pour en savoir plus sur ce sous-sujet obscur.Le reste de cet article ne concerne pas cette obscure fonctionnalité de GNU C.

Norme C (c'est-à-direnon calculé) les gotos ne sont généralement pas la raison pour laquelle le code inaccessible ne peut pas être trouvé au moment de la compilation.La raison habituelle est un code logique comme celui-ci.Donné

int computation1() {
  return 1;
}

int computation2() {
  return computation1();
}

Il est tout aussi difficile pour un compilateur de trouver du code inaccessible dans l'une des trois constructions suivantes :

void tough1() {
  if (computation1() != computation2())
    printf("Unreachable\n");
}

void tough2() {
  if (computation1() == computation2())
    goto out;
  printf("Unreachable\n");
out:;
}

struct Out{};

void tough3() {
  try {
    if (computation1() == computation2())
      throw Out();
    printf("Unreachable\n");
  }
  catch (Out&) {
  }
}

(Excusez mon style de codage lié aux accolades, mais j'ai essayé de garder les exemples aussi compacts que possible.)

Visual C++ /W4 (même avec /Ox) ne parvient pas à trouver du code inaccessible dans aucun de ces éléments, et comme vous le savez probablement, le problème de la recherche de code inaccessible est indécidable en général.(Si vous ne me croyez pas sur ce point : https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)

De manière connexe, le C goto peut être utilisé pour émuler des exceptions uniquement à l'intérieur du corps d'une fonction.La bibliothèque C standard propose une paire de fonctions setjmp() et longjmp() pour émuler des sorties/exceptions non locales, mais celles-ci présentent de sérieux inconvénients par rapport à ce que proposent d'autres langages.L'article Wikipédia http://en.wikipedia.org/wiki/Setjmp.h explique assez bien cette dernière question.Cette paire de fonctions fonctionne également sous Windows (http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx), mais presque personne ne les utilise là-bas car SEH/VEH est supérieur.Même sous Unix, je pense que setjmp et longjmp sont très rarement utilisés.

2) Je pense que la deuxième utilisation la plus courante de goto en C est l'implémentation d'une pause multi-niveaux ou d'une continuation multi-niveaux, ce qui est également un cas d'utilisation assez peu controversé.Rappelez-vous que Java n'autorise pas l'étiquette goto, mais autorise l'étiquette break ou l'étiquette continue.Selon http://www.oracle.com/technetwork/java/simple-142616.html, c'est en fait le cas d'utilisation le plus courant des gotos en C (90% disent-ils), mais d'après mon expérience subjective, le code système a tendance à utiliser plus souvent les gotos pour la gestion des erreurs.Peut-être que dans le code scientifique ou lorsque le système d'exploitation propose la gestion des exceptions (Windows), les sorties à plusieurs niveaux constituent le cas d'utilisation dominant.Ils ne donnent pas vraiment de détails quant au contexte de leur enquête.

Modifié pour ajouter :il s'avère que ces deux modèles d'utilisation se trouvent dans le livre C de Kernighan et Ritchie, vers la page 60 (selon l'édition).Une autre chose à noter est que les deux cas d'utilisation impliquent uniquement des gotos avancés.Et il s'avère que l'édition MISRA C 2012 (contrairement à l'édition 2004) autorise désormais les gotos, à condition qu'ils ne soient que des forwards.

En Perl, utilisation d'une étiquette pour "aller à" à partir d'une boucle - en utilisant une instruction "last", qui est similaire à break.

Cela permet un meilleur contrôle sur les boucles imbriquées.

Le goto traditionnel étiquette est également pris en charge, mais je ne suis pas sûr qu'il y ait trop de cas où c'est le seul moyen d'obtenir ce que vous voulez - les sous-programmes et les boucles devraient suffire dans la plupart des cas.

Le problème avec « goto » et l'argument le plus important du mouvement « programmation sans goto » est que si vous l'utilisez trop fréquemment, votre code, même s'il peut se comporter correctement, devient illisible, impossible à maintenir, non révisable, etc.Dans 99,99% des cas, « goto » mène au code spaghetti.Personnellement, je ne vois aucune bonne raison pour laquelle j'utiliserais « goto ».

Edsger Dijkstra, un informaticien ayant apporté d'importantes contributions dans le domaine, était également célèbre pour avoir critiqué l'utilisation de GoTo.Il y a un court article sur son argument sur Wikipédia.

J'utilise goto dans le cas suivant :lorsque nécessaire pour revenir de fonctions à différents endroits, et avant le retour, une certaine désinitialisation doit être effectuée :

version non accessible :

int doSomething (struct my_complicated_stuff *ctx)    
{
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
        db_disconnect(conn);
        return -1;      
        }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        free(temp_data);    
        db_disconnect(conn);    
        return -2;
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        pthread_mutex_unlock(ctx->mutex);
        free(temp_data);
        db_disconnect(conn);        
        return -3;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -4;  
    }

    if (ctx->something_else->additional_check) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -5;  
    }


    pthread_mutex_unlock(ctx->mutex);
    free(temp_data);    
    db_disconnect(conn);    
    return 0;     
}

aller à la version :

int doSomething_goto (struct my_complicated_stuff *ctx)
{
    int ret=0;
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
            ret=-1;
           goto exit_db;   
          }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        ret=-2;
        goto exit_freetmp;      
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        ret=-3;
        goto exit;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
        ret=-4;
        goto exit_freekey; 
    }

    if (ctx->something_else->additional_check) {
        ret=-5;
        goto exit_freekey;  
    }

exit_freekey:
    rsa_free(key);
exit:    
    pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
    free(temp_data);        
exit_db:
    db_disconnect(conn);    
    return ret;     
}

La deuxième version facilite la tâche lorsque vous devez modifier quelque chose dans les instructions de désallocation (chacune est utilisée une fois dans le code) et réduit le risque d'ignorer l'une d'entre elles lors de l'ajout d'une nouvelle branche.Les déplacer dans une fonction n'aidera pas ici, car la désallocation peut se faire à différents "niveaux".

Certains disent qu'il n'y a aucune raison de goto en C++.Certains disent que dans 99 % des cas, il existe de meilleures alternatives. Ce n’est pas un raisonnement, juste des impressions irrationnelles. Voici un exemple solide où goto mène à un joli code, quelque chose comme une boucle do-while améliorée :

int i;

PROMPT_INSERT_NUMBER:
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    goto PROMPT_INSERT_NUMBER;          
  }

std::cout << "your number is " << i;

Comparez-le au code goto-free :

int i;

bool loop;
do {
  loop = false;
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    loop = true;          
  }
} while(loop);

std::cout << "your number is " << i;

Je vois ces différences :

  • imbriqué {} un bloc est nécessaire (bien que do {...} while ça a l'air plus familier)
  • supplémentaire loop une variable est nécessaire, utilisée à quatre endroits
  • il faut plus de temps pour lire et comprendre le travail avec le loop
  • le loop ne contient aucune donnée, il contrôle juste le flux de l'exécution, ce qui est moins compréhensible qu'une simple étiquette

Il y a un autre exemple

void sort(int* array, int length) {
SORT:
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    goto SORT; // it is very easy to understand this code, right?
  }
}

Maintenant, débarrassons-nous du « mal » goto :

void sort(int* array, int length) {
  bool seemslegit;
  do {
    seemslegit = true;
    for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
      swap(data[i], data[i+1]);
      seemslegit = false;
    }
  } while(!seemslegit);
}

Vous voyez, c'est le même type d'utilisation de goto, c'est un modèle bien structuré et ce n'est pas autant de promouvoir goto que la seule méthode recommandée.Vous voulez sûrement éviter le code « intelligent » comme celui-ci :

void sort(int* array, int length) {
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    i = -1; // it works, but WTF on the first glance
  }
}

Le fait est que goto peut être facilement utilisé à mauvais escient, mais goto lui-même n’est pas à blâmer.Notez que label a une portée de fonction en C++, donc elle ne pollue pas la portée globale comme dans l'assembly pur, dans lequel boucles superposées ont leur place et sont très courants - comme dans le code suivant pour 8051, où l'affichage à 7 segments est connecté à P1.Le programme boucle un segment éclair autour de :

; P1 states loops
; 11111110 <-
; 11111101  |
; 11111011  |
; 11110111  |
; 11101111  |
; 11011111  |
; |_________|

init_roll_state:
    MOV P1,#11111110b
    ACALL delay
next_roll_state:
    MOV A,P1
    RL A
    MOV P1,A
    ACALL delay
    JNB P1.5, init_roll_state
    SJMP next_roll_state

Il y a un autre avantage :goto peut servir de boucles nommées, de conditions et d'autres flux :

if(valid) {
  do { // while(loop)

// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket

  } while(loop);
} // if(valid)

Ou vous pouvez utiliser un goto équivalent avec une indentation, vous n'avez donc pas besoin de commentaire si vous choisissez judicieusement le nom de l'étiquette :

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top