web-dev-qa-db-fra.com

Comment éviter l'utilisation de goto et rompre efficacement les boucles imbriquées

Je dirais que c'est un fait que l'utilisation de goto est considérée comme une mauvaise pratique en matière de programmation en C/C++.

Cependant, étant donné le code suivant

for (i = 0; i < N; ++i) 
{
    for (j = 0; j < N; j++) 
    {
        for (k = 0; k < N; ++k) 
        {
            ...
            if (condition)
                goto out;
            ...
        }
    }
}
out:
    ...

Je me demande comment obtenir efficacement le même comportement sans utiliser goto. Ce que je veux dire, c'est que nous pourrions faire quelque chose comme vérifier condition à la fin de chaque boucle, par exemple, mais AFAIK goto générera une seule instruction Assembly qui sera une jmp. C'est donc le moyen le plus efficace de le faire auquel je puisse penser.

Existe-t-il une autre pratique considérée comme une bonne pratique? Est-ce que je me trompe quand je dis que c'est considéré comme une mauvaise pratique d'utiliser goto? Si je le suis, s'agirait-il d'un de ces cas où il est bon de l'utiliser?

Je vous remercie

30
rual93

La meilleure version non-goto (imo) ressemblerait à ceci:

void calculateStuff()
{
    // Please use better names than this.
    doSomeStuff();
    doLoopyStuff();
    doMoreStuff();
}

void doLoopyStuff()
{
    for (i = 0; i < N; ++i) 
    {
        for (j = 0; j < N; j++) 
        {
            for (k = 0; k < N; ++k) 
            {
                /* do something */

                if (/*condition*/)
                    return; // Intuitive control flow without goto

                /* do something */
            }
        }
    }
}

Le fractionnement est également une bonne idée car cela vous permet de garder vos fonctions courtes, votre code lisible (si vous nommez les fonctions mieux que moi) et vos dépendances faibles.

47
Max Langhof

Si vous avez des boucles profondément imbriquées comme celle-ci et que vous devez éclater, je pense que goto est la meilleure solution. Certaines langues (pas C) ont une instruction break(N) qui sortira de plusieurs boucles. La raison pour laquelle C ne l’a pas, c’est parce que c’est même pire qu’un goto: vous devez compter les boucles imbriquées pour comprendre ce qu’il fait, et il est donc vulnérable que quelqu'un vienne plus tard et ajoute ou supprime un niveau de imbrication, sans remarquer que le nombre de pauses doit être ajusté.

Oui, gotos sont généralement mal vues. L'utilisation de goto n'est pas une bonne solution. c'est simplement le moindre de plusieurs maux.

Dans la plupart des cas, la raison pour laquelle vous devez sortir d'une boucle profondément imbriquée est que vous recherchez quelque chose et que vous l'avez trouvé. Dans ce cas (et comme plusieurs autres commentaires et réponses l'ont suggéré), je préfère déplacer la boucle imbriquée vers sa propre fonction. Dans ce cas, un return de la boucle interne accomplit votre tâche de manière très propre.

(Il y a ceux qui disent que les fonctions doivent toujours revenir à la fin, pas au milieu. Ces personnes diraient que la solution facile de casser-le-à-une-fonction est donc invalide, et elles forceraient l'utilisation même technique de rupture de la boucle intérieure, même lorsque la recherche a été scindée pour son propre fonctionnement. Personnellement, je pense que ces personnes ont tort, mais leur kilométrage peut varier.)

Si vous insistez pour ne pas utiliser goto et si vous n'utilisez pas une fonction distincte avec retour anticipé, vous pouvez effectivement, par exemple, gérer des variables de contrôle booléennes supplémentaires et les tester de manière redondante dans la condition de contrôle de chaque boucle imbriquée, mais c'est juste une nuisance et un gâchis. (C’est l’un des plus grands maux que je disais en utilisant un simple goto est moindre que.)

22
Steve Summit

Je pense que goto est une chose parfaitement saine d'esprit ici, et constitue l'un de ses cas d'utilisation exceptionnels selon le C++ Core Guidelines .

Cependant, une autre solution à envisager est peut-être un iife _ lambda. À mon avis, cela est légèrement plus élégant que de déclarer une fonction distincte!

[&] {
    for (int i = 0; i < N; ++i)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; ++k)
                if (condition)
                    return;
}();

Merci à JohnMcPineapple sur reddit pour cette suggestion!

15
ricco19

Lambdas vous permet de créer des portées locales:

[&]{
  for (i = 0; i < N; ++i) 
  {
    for (j = 0; j < N; j++) 
    {
      for (k = 0; k < N; ++k) 
      {
        ...
        if (condition)
          return;
        ...
      }
    }
  }
}();

si vous voulez aussi pouvoir retourner hors de cette portée:

if (auto r = [&]()->boost::optional<RetType>{
  for (i = 0; i < N; ++i) 
  {
    for (j = 0; j < N; j++) 
    {
      for (k = 0; k < N; ++k) 
      {
        ...
        if (condition)
          return {};
        ...
      }
    }
  }
}()) {
  return *r;
}

où renvoyer {} ou boost::nullopt est un "break", et renvoyer une valeur renvoie une valeur de la portée englobante.

Une autre approche est:

for( auto idx : cube( {0,N}, {0,N}, {0,N} ) {
  auto i = std::get<0>(idx);
  auto j = std::get<1>(idx);
  auto k = std::get<2>(idx);
}

où nous générons un itérable sur toutes les 3 dimensions et en faisons une boucle imbriquée 1 profonde. Maintenant, break fonctionne bien. Vous devez écrire cube.

En c ++ 17 cela devient

for( auto[i,j,k] : cube( {0,N}, {0,N}, {0,N} ) ) {
}

qui est Nice.

Maintenant, dans une application où vous êtes censé être réactif, la boucle sur une large plage tridimensionnelle au niveau du flux de contrôle primaire est souvent une mauvaise idée. Vous pouvez le filer, mais même dans ce cas, vous vous retrouvez avec le problème que le fil dure trop longtemps. Et la plupart des grandes itérations en 3 dimensions avec lesquelles j'ai joué peuvent tirer profit de l'utilisation du filetage des sous-tâches elles-mêmes.

À cette fin, vous finirez par vouloir catégoriser votre opération en fonction du type de données auquel elle accède, puis passez votre opération à un élément planifiant l'itération pour vous.

auto work = do_per_Voxel( volume,
  [&]( auto&& Voxel ) {
    // do work on the Voxel
    if (condition)
      return Worker::abort;
    else
      return Worker::success;
  }
);

alors le flux de contrôle impliqué passe dans la fonction do_per_Voxel.

do_per_Voxel ne va pas être une simple boucle nue, mais plutôt un système pour réécrire les tâches par Voxel en tâches par ligne de balayage (ou même des tâches par plan selon la taille des plans/lignes de balayage au moment de l'exécution (!)) puis répartissez-les tour à tour dans un planificateur de tâches gérées de pool de threads, assemblez les poignées de tâches résultantes et renvoyez une variable work de type futur qui peut être attendue ou utilisée comme déclencheur de continuation pour l'exécution du travail.

Et parfois, vous utilisez simplement goto. Ou alors, vous définissez manuellement les fonctions des sous-topos. Ou vous utilisez des drapeaux pour sortir d'une récursion profonde. Ou vous mettez la boucle de 3 couches entière dans sa propre fonction. Ou vous composez les opérateurs de boucle en utilisant une bibliothèque monad. Ou vous pouvez lancer une exception (!) Et l'attraper.

La réponse à presque chaque question dans c ++ est "ça dépend". La portée du problème et le nombre de techniques dont vous disposez sont vastes et les détails du problème modifient les détails de la solution.

Alternative - 1

Vous pouvez faire quelque chose comme suit:

  1. Définir une variable bool au début isOkay = true
  2. Tous vos forloop conditions, ajoutez une condition supplémentaire isOkay == true
  3. Lorsque votre condition personnalisée est satisfaite/échoue, définissez isOkay = false

Cela fera arrêter vos boucles. Une variable supplémentaire bool serait cependant parfois utile.

bool isOkay = true;
for (int i = 0; isOkay && i < N; ++i)
{
    for (int j = 0; isOkay && j < N; j++)
    {
        for (int k = 0; isOkay && k < N; ++k)
        {
            // some code
            if (/*your condition*/)
                 isOkay = false;
        }
     }
}

Alternative - 2

Deuxièmement. Si les itérations de boucle ci-dessus sont dans une fonction, le meilleur choix est de return résultat, chaque fois que la condition personnalisée est remplie.

bool loop_fun(/* pass the array and other arguments */)
{
    for (int i = 0; i < N ; ++i)
    {
        for (int j = 0; j < N ; j++)
        {
            for (int k = 0; k < N ; ++k)
            {
                // some code
                if (/* your condition*/)
                    return false;
            }
        }
    }
    return true;
}
4
JeJo

Divisez vos boucles for en fonctions ..__ Cela rend les choses beaucoup plus faciles à comprendre car vous pouvez maintenant voir ce que chaque boucle fait réellement.

bool doHerpDerp() {
    for (i = 0; i < N; ++i) 
    {
        if (!doDerp())
            return false;
    }
    return true;
}

bool doDerp() {
    for (int i=0; i<X; ++i) {
        if (!doHerp())
            return false;
    }
    return true;
}

bool doHerp() {
    if (shouldSkip)
        return false;
    return true;
}
3
UKMonkey

Existe-t-il une autre pratique considérée comme une bonne pratique? Ai-je tort quand Je dis que c'est considéré comme une mauvaise pratique d'utiliser goto?

goto peut être mal utilisé ou trop souvent utilisé, mais je ne vois aucun des deux dans votre exemple. La sortie d'une boucle profondément imbriquée est le plus clairement exprimée par un simple goto label_out_of_the_loop;

Il est déconseillé d’utiliser de nombreux gotos qui accèdent à des libellés différents, mais dans ce cas, ce n’est pas le mot clé goto qui rend le code incorrect. C'est le fait que vous sautez dans le code, ce qui rend difficile à suivre qui le rend mauvais. Si toutefois, vous avez besoin d'un seul saut hors des boucles imbriquées, pourquoi ne pas utiliser l'outil créé à cette fin.

Pour utiliser une analogie inventée à partir de rien, imaginez: vous vivez dans un monde où jadis il était hanté de planter des clous dans les murs. Ces derniers temps, il est devenu plus facile de percer des vis dans les murs à l'aide de tournevis et les marteaux sont complètement démodés. Maintenant, considérez que vous devez (en dépit d'être un peu vieux-fashinoned) obtenir un clou dans un mur. Vous ne devriez pas vous abstenir d'utiliser un marteau pour le faire, mais vous devriez peut-être plutôt vous demander si vous avez vraiment besoin d'un clou dans le mur au lieu d'une vis. 

(Juste au cas où ce ne serait pas clair: le marteau est goto et le clou dans le mur est un saut d'une boucle imbriquée alors que la vis dans le mur utiliserait des fonctions pour éviter le nid en profondeur;)

3
user463035818

Spécifique

OMI, dans cet exemple spécifique, je pense qu'il est important de noter une fonctionnalité commune entre vos boucles. (Maintenant, je sais que votre exemple n'est pas nécessairement littéral ici, mais tenez compte de moi pendant une seconde) car chaque boucle itère N fois, vous pouvez restructurer votre code de la manière suivante:

Exemple

int max_iterations = N * N * N;
for (int i = 0; i < max_iterations; i++)
{
    /* do stuff, like the following for example */
    *(some_ptr + i) = 0; // as opposed to *(some_3D_ptr + i*X + j*Y + Z) = 0;
    // some_arr[i] = 0; // as oppose to some_3D_arr[i][j][k] = 0;
}

Maintenant, il est important de se rappeler que toutes les boucles, qu'elles soient pour ou non, ne sont en réalité que du sucre syntatique pour le paradigme if-goto. Je suis d’accord avec les autres pour dire que vous devriez avoir pour fonction de renvoyer le résultat, mais j’ai voulu montrer un exemple comme celui-ci, dans lequel ce n’est peut-être pas le cas. Certes, je signalerais ce qui précède dans une révision de code, mais si vous remplaçiez ce qui précède par un goto, je considérerais cela comme un pas dans la mauvaise direction. (NOTE - Assurez-vous que vous pouvez l’intégrer de manière fiable dans le type de données souhaité)

Général

Maintenant, en règle générale, les conditions de sortie de votre boucle peuvent ne pas être identiques à chaque fois (comme pour le message en question). En règle générale, retirez autant que vous le pouvez le nombre d'opérations inutiles dans vos boucles (multiplications, etc.), alors que les compilateurs deviennent de plus en plus intelligents, rien ne remplace l'écriture de code efficace et lisible. 

Exemple

/* matrix_length: int of m*n (row-major order) */
int num_squared = num * num;
for (int i = 0; i < matrix_length; i++)
{
    some_matrix[i] *= num_squared; // some_matrix is a pointer to an array of ints of size matrix_length
}

plutôt que d'écrire *= num * num, nous n'avons plus besoin de compter sur le compilateur pour l'optimiser (pour nous, tout bon compilateur devrait l'être). Ainsi, toute boucle double ou triple imbriquée qui exécute la fonctionnalité ci-dessus serait également bénéfique non seulement pour votre code, mais également pour l’OMI qui rédigera du code propre et efficace. Dans le premier exemple, nous aurions pu utiliser plutôt *(some_3D_ptr + i*X + j*Y + Z) = 0;! Faisons-nous confiance au compilateur pour optimiser les i*X et j*Y, afin qu’ils ne soient pas calculés à chaque itération?

bool check_threshold(int *some_matrix, int max_value)
{
    for (int i = 0; i < rows; i++)
    {
        int i_row = i*cols; // no longer computed inside j loop unnecessarily.
        for (int j = 0; j < cols; j++)
        {
            if (some_matrix[i_row + j] > max_value) return true;
        }
    }
    return false;
}

Beurk! Pourquoi n'utilisons-nous pas des classes fournies par la STL ou une bibliothèque comme Boost? (nous devons faire ici du code de bas niveau/très performant). Je ne pouvais même pas écrire une version 3D, à cause de la complexité. Même si nous avons quelque chose d’optimisé à la main, il peut même être préférable d’utiliser #pragma unroll ou des astuces de préprocesseur similaires si votre compilateur le permet.

Conclusion

En règle générale, plus le niveau d'abstraction que vous pouvez utiliser est élevé, mieux c'est. Cependant, si vous croyez aliaser une matrice d'entiers de rangées majeure à une dimension en un tableau bidimensionnel, il est plus difficile à comprendre/à étendre votre flux de code. ? De même, cela peut aussi être un indicateur pour faire de quelque chose sa propre fonction. J'espère que, grâce à ces exemples, vous constaterez que différents paradigmes sont nécessaires à différents endroits et que c'est à vous de le faire en tant que programmeur. Ne soyez pas fous avec ce qui précède, mais assurez-vous de savoir ce qu’ils veulent dire, comment les utiliser et quand ils sont appelés, et surtout, assurez-vous que les autres personnes qui utilisent votre base de code le savent aussi bien et ont aucun scrupule à ce sujet. Bonne chance!

2
jfh

en ce qui concerne votre commentaire sur l'efficacité, la compilation des deux options en mode édition sur Visual Studio 2017 produit exactement le même assemblage. 

for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                goto end;

            }
        }
    }
}
end:;

et avec un drapeau.

bool done = false;
for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                done = true;
                break;
            }
        }
        if (done) break;
    }
    if (done) break;
}

les deux produisent ..

xor         edx,edx  
xor         ecx,ecx  
xor         eax,eax  
cmp         edx,1  
jne         main+15h (0C11015h)  
cmp         ecx,2  
jne         main+15h (0C11015h)  
cmp         eax,3  
je          main+27h (0C11027h)  
inc         eax  
cmp         eax,5  
jl          main+6h (0C11006h)  
inc         ecx  
cmp         ecx,5  
jl          main+4h (0C11004h)  
inc         edx  
cmp         edx,5  
jl          main+2h (0C11002h)    

donc il n'y a pas de gain. Une autre option si vous utilisez un compilateur c ++ moderne consiste à l’envelopper dans un lambda.

[](){
for (int i = 0; i < 5; ++i)
{
    for (int j = 0; j < 5; ++j)
    {
        for (int k = 0; k < 5; ++k)
        {
            if (i == 1 && j == 2 && k == 3) {
                return;
            }
        }

    }
}
}();

encore une fois, cela produit exactement la même assemblée. Personnellement, je pense que l’utilisation de goto dans votre exemple est parfaitement acceptable. Il est clair que ce qui arrive à quiconque et rend le code plus concis. On peut soutenir que le lambda est tout aussi concis.

2
rmawatson

Il existe déjà plusieurs excellentes réponses qui vous expliquent comment modifier votre code, je ne les répéterai donc pas. Il n’est plus nécessaire de coder de cette façon pour plus d’efficacité; la question est de savoir si c'est inélégant. (Un raffinement que je vais suggérer: si vos fonctions d’aide sont uniquement destinées à être utilisées dans le corps de cette fonction, vous pouvez aider l’optimiseur en les déclarant static, afin qu’il sache avec certitude que la fonction ne fonctionne pas. avez un lien externe et ne sera jamais appelé à partir d'un autre module, et l'indice inline ne peut pas faire de mal. Cependant, les réponses précédentes indiquent que, lorsque vous utilisez un lambda, les compilateurs modernes n'ont pas besoin de tels conseils.)

Je vais contester un peu le cadrage de la question. Vous avez raison de dire que la plupart des programmeurs ont un tabou contre l'utilisation de goto. Cela a, à mon avis, perdu de vue le but initial. Lorsque Edsger Dijkstra écrivit: «Déclaration jugée nuisible», il avait une raison bien précise de le penser: l'utilisation «débridée» de go to rend trop difficile de raisonner formellement sur l'état actuel du programme et sur les conditions qui doivent être remplies, comparé au flux de contrôle des appels de fonction récursifs (qu'il a préférés) ou des boucles itératives (qu'il a acceptées). Il a conclu:

L'instruction go to telle qu'elle est est tout simplement trop primitive; c'est trop une invitation à gâcher son programme. On peut considérer et apprécier les clauses considérées comme un frein à son utilisation. Je ne prétends pas que les clauses mentionnées sont exhaustives en ce sens qu'elles satisferont à tous les besoins, mais quelles que soient les clauses suggérées (par exemple, les clauses d'avortement), elles devraient satisfaire à l'exigence selon laquelle un système de coordonnées indépendant du programmeur peut être maintenu pour décrire le processus de manière adéquate. manière utile et gérable. 

De nombreux langages de programmation de type C, tels que Rust et Java, comportent une «clause considérée comme un moyen de limiter leur utilisation», la variable break à une étiquette. Une syntaxe encore plus restreinte pourrait être quelque chose comme break 2 continue; pour sortir de deux niveaux de la boucle imbriquée et reprendre en haut de la boucle qui les contient. Cela ne pose pas plus de problème qu'un C-style break à ce que Dijkstra a voulu faire: définir une description concise de l'état du programme que les programmeurs peuvent garder dans leur tête ou qu'un analyseur statique pourrait traiter.

La limitation de goto à des constructions comme celle-ci en fait simplement une rupture renommée en étiquette. Le problème qui reste est que le compilateur et le programmeur ne savent pas nécessairement que vous allez l’utiliser de cette façon.

S'il existe une post-condition importante qui se maintient après la boucle et que votre préoccupation concernant goto est identique à celle de Dijkstra, vous pouvez envisager de l'exprimer dans un court commentaire, quelque chose comme // We have done foo to every element, or encountered condition and stopped. qui atténuerait le problème pour les humains, et un analyseur statique devrait fais bien.

1
Davislor

Une méthode possible consiste à affecter une valeur booléenne à une variable représentant l'état. Cet état peut être testé ultérieurement à l'aide d'une instruction conditionnelle "IF" à d'autres fins ultérieurement dans le code.

1
Chigozie Orunta
bool meetCondition = false;
for (i = 0; i < N && !meetCondition; ++i) 
{
    for (j = 0; j < N && !meetCondition; j++) 
    {
        for (k = 0; k < N && !meetCondition; ++k) 
        {
            ...
            if (condition)
                meetCondition = true;
            ...
        }
    }
}
1
nameless1

La meilleure solution consiste à insérer les boucles dans une fonction, puis à return à partir de cette fonction. 

C’est essentiellement la même chose que votre exemple goto, mais avec le avantage énorme vous évitez d’avoir un autre débat goto.

Pseudo code simplifié:

bool function (void)
{
  bool result = something;


  for (i = 0; i < N; ++i) 
    for (j = 0; j < N; j++) 
      for (k = 0; k < N; ++k) 
        if (condition)
          return something_else;
  ...
  return result;
}

Un autre avantage est que vous pouvez passer de bool à enum si vous rencontrez plus de 2 scénarios. Vous ne pouvez pas vraiment faire cela avec goto de manière lisible. Le moment où vous commencez à utiliser plusieurs gotos et plusieurs étiquettes est le moment où vous adoptez le codage spaghetti. Oui, même si vous n’avez qu’une branche en bas, ce ne sera pas beau à lire et à maintenir.

Notamment, si vous avez 3 boucles imbriquées, cela peut indiquer que vous devriez essayer de scinder votre code en plusieurs fonctions et que toute cette discussion risque de ne même pas être pertinente.

0
Lundin