Quelles sont les meilleures façons d'éviter le do-while (0); pirater en C ++?


233

Lorsque le flux de code est comme ceci:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

J'ai généralement vu ce travail pour éviter le flux de code désordonné ci-dessus:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Quelles sont les meilleures façons d'éviter ce contournement / piratage afin qu'il devienne un code de niveau supérieur (niveau industriel)?

Toutes les suggestions qui sont hors de la boîte sont les bienvenues!


38
RAII et lever des exceptions.
ta.speot.is

135
Pour moi, cela semble être un bon moment à utiliser goto- mais je suis sûr que quelqu'un me notera pour avoir suggéré cela, donc je n'écris pas de réponse à cet effet. Avoir un LONG do ... while(0);semble être la mauvaise chose.
Mats Petersson

42
@dasblinkenlight: Oui, en effet. Si vous allez utiliser goto, soyez honnête à ce sujet et faites-le en plein air, ne le cachez pas en utilisant breaketdo ... while
Mats Petersson

44
@MatsPetersson: Faites-en une réponse, je vous donnerai +1 pour votre gotoeffort de réhabilitation. :)
wilx

27
@ ta.speot.is: "RAII et lever les exceptions". En faisant cela, vous émulerez le contrôle de flux avec des exceptions. C'est-à-dire que c'est un peu comme utiliser du matériel coûteux et à la pointe de la technologie comme marteau ou presse-papier. Vous pouvez le faire, mais cela ressemble vraiment à un très mauvais goût pour moi.
SigTerm

Réponses:


310

Il est considéré comme une pratique acceptable d'isoler ces décisions dans une fonction et d'utiliser returns au lieu de breaks. Bien que toutes ces vérifications correspondent au même niveau d'abstraction que pour la fonction, c'est une approche assez logique.

Par exemple:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}

22
@MatsPetersson: "Isoler dans la fonction" signifie refactoriser dans une nouvelle fonction qui ne fait que les tests.
MSalters

35
+1. C'est aussi une bonne réponse. En C ++ 11, la fonction isolée peut être un lambda, car elle peut également capturer les variables locales, ce qui facilite les choses!
Nawaz

4
@deworde: Selon la situation, cette solution peut devenir beaucoup plus longue et moins lisible que goto. Parce que C ++, malheureusement, ne permet pas les définitions de fonctions locales, vous devrez déplacer cette fonction (celle dont vous allez revenir) ailleurs, ce qui réduit la lisibilité. Cela pourrait se retrouver avec des dizaines de paramètres, puis, parce qu'avoir des dizaines de paramètres est une mauvaise chose, vous déciderez de les envelopper dans struct, ce qui créera un nouveau type de données. Trop de frappe pour une situation simple.
SigTerm

24
@Damon returnest plus propre, car tout lecteur est immédiatement conscient qu'il fonctionne correctement et ce qu'il fait. Avec gotovous devez regarder autour de vous pour voir à quoi cela sert et être sûr qu'aucune erreur n'a été commise. Le déguisement est l'avantage.
R. Martinho Fernandes

11
@SigTerm: Parfois, le code doit être déplacé vers une fonction distincte juste pour garder la taille de chaque fonction à une taille facile à expliquer. Si vous ne voulez pas payer pour un appel de fonction, marquez-le forceinline.
Ben Voigt

257

Il y a des moments où utiliser gotoest en fait la bonne réponse - au moins pour ceux qui ne sont pas élevés dans la croyance religieuse qui " gotone peut jamais être la réponse, quelle que soit la question" - et c'est l'un de ces cas.

Ce code utilise le hack de do { ... } while(0);dans le seul but d'habiller un gotocomme un break. Si vous allez utiliser goto, alors soyez ouvert à ce sujet. Il est inutile de rendre le code PLUS DUR à lire.

Une situation particulière se produit juste lorsque vous avez beaucoup de code avec des conditions assez complexes:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

Cela peut en fait rendre plus clair que le code est correct en n'ayant pas une logique aussi complexe.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

Edit: Pour clarifier, je ne propose en aucun cas l'utilisation de gotocomme solution générale. Mais il y a des cas où il gotoy a une meilleure solution que d'autres solutions.

Imaginez par exemple que nous collectons des données, et que les différentes conditions testées sont une sorte de "c'est la fin des données collectées" - qui dépend d'une sorte de marqueurs "continuer / terminer" qui varient selon l'endroit vous êtes dans le flux de données.

Maintenant, lorsque nous avons terminé, nous devons enregistrer les données dans un fichier.

Et oui, il existe souvent d'autres solutions qui peuvent fournir une solution raisonnable, mais pas toujours.


76
Être en désaccord. gotopeut avoir une place, mais goto cleanuppas. Le nettoyage se fait avec RAII.
MSalters

14
@MSalters: Cela suppose que le nettoyage implique quelque chose qui peut être résolu avec RAII. Peut-être aurais-je dû dire "problème d'erreur" ou quelque chose comme ça à la place.
Mats Petersson

25
Alors, continuez à recevoir des votes négatifs sur celui-ci, probablement de ceux de la croyance religieuse de goton'est jamais la bonne réponse. J'apprécierais s'il y avait un commentaire ...
Mats Petersson

19
les gens détestent gotoparce que vous devez penser à l'utiliser / à comprendre un programme qui l'utilise ... Les microprocesseurs, d'autre part, sont construits jumpset conditional jumps... donc le problème est avec certaines personnes, pas avec la logique ou autre chose.
woliveirajr

17
+1 pour une utilisation appropriée de goto. RAII n'est pas la bonne solution si des erreurs sont attendues (pas exceptionnelles), car ce serait un abus des exceptions.
Joe

82

Vous pouvez utiliser un modèle de continuation simple avec une boolvariable:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

Cette chaîne d'exécution s'arrêtera dès que checkNretourne a false. Aucun autre check...()appel ne serait effectué en raison d'un court-circuit de l' &&opérateur. De plus, l'optimisation des compilateurs est suffisamment intelligente pour reconnaître que le réglage goOnsur falseest à sens unique et insérer les éléments manquants goto endpour vous. En conséquence, les performances du code ci-dessus seraient identiques à celles d'un do/ while(0), sans un coup douloureux à sa lisibilité.


30
Les affectations dans des ifconditions semblent très suspectes.
Mikhail

90
@Mikhail Seulement pour un œil inexpérimenté.
dasblinkenlight

20
J'ai déjà utilisé cette technique auparavant et cela m'a toujours un peu dérangé de penser que le compilateur devrait générer du code pour vérifier chacun, ifpeu importe ce qui se goOnpassait même si un premier échouait (par opposition à sauter / éclater) ... mais je viens de faire un test et VS2012 au moins était assez intelligent pour tout court-circuiter après le premier faux de toute façon. Je vais l'utiliser plus souvent. Remarque: si vous utilisez goOn &= checkN()alors, checkN()il s'exécutera toujours même s'il goOnétait falseau début du if(c.-à-d. ne le faites pas).
marquez

11
@Nawaz: Si vous avez un esprit inexpérimenté qui effectue des changements arbitraires partout dans une base de code, vous avez un problème beaucoup plus important que les affectations à l'intérieur de ifs.
Idelic

6
@sisharp Elegance est tellement dans l'œil du spectateur! Je n'arrive pas à comprendre comment dans le monde une mauvaise utilisation d'une construction en boucle pourrait en quelque sorte être perçue comme "élégante", mais c'est peut-être juste moi.
dasblinkenlight

38
  1. Essayez d'extraire le code dans une fonction distincte (ou peut-être plus d'une). Revenez ensuite de la fonction si la vérification échoue.

  2. S'il est trop étroitement couplé avec le code environnant pour le faire, et que vous ne pouvez pas trouver un moyen de réduire le couplage, regardez le code après ce bloc. Vraisemblablement, il nettoie certaines ressources utilisées par la fonction. Essayez de gérer ces ressources à l'aide d'un objet RAII ; puis remplacez chaque douteux breakpar return(ou throw, si cela est plus approprié) et laissez le destructeur de l'objet nettoyer pour vous.

  3. Si le déroulement du programme est (nécessairement) si cahoteux que vous en avez vraiment besoin goto, utilisez-le plutôt que de lui donner un déguisement bizarre.

  4. Si vous avez des règles de codage qui l'interdisent aveuglément gotoet que vous ne pouvez vraiment pas simplifier le flux du programme, vous devrez probablement le masquer avec votre dohack.


4
Je soumets humblement que le RAII, bien qu'utile, n'est pas une solution miracle. Lorsque vous vous trouvez sur le point d'écrire une classe convert-goto-to-RAII qui n'a aucune autre utilité, je pense vraiment que vous seriez mieux servi simplement en utilisant l'idiome "goto-end-of-the-world" déjà mentionné.
idobie

1
@busy_wait: En effet, RAII ne peut pas tout résoudre; c'est pourquoi ma réponse ne s'arrête pas au deuxième point, mais suggère ensuite gotosi c'est vraiment une meilleure option.
Mike Seymour

3
Je suis d'accord, mais je pense que c'est une mauvaise idée d'écrire des classes de conversion goto-RAII et je pense que cela devrait être indiqué explicitement.
idobie

@idoby qu'en est-il de l'écriture d' une classe de modèle RAII et de l'instanciation avec le nettoyage à usage unique dont vous avez besoin dans un lambda
Caleth

@Caleth me semble que vous réinventez des ctors / dtors?
idobie le

37

TLDR : RAII , code transactionnel (définir uniquement les résultats ou renvoyer des éléments lorsqu'ils sont déjà calculés) et exceptions.

Longue réponse:

En C , la meilleure pratique pour ce type de code consiste à ajouter une étiquette EXIT / CLEANUP / other dans le code, où le nettoyage des ressources locales se produit et un code d'erreur (le cas échéant) est renvoyé. C'est la meilleure pratique car elle divise le code naturellement en initialisation, calcul, validation et retour:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

En C, dans la plupart codebases, le if(error_ok != ...et le gotocode est généralement caché derrière des macros ci ( RET(computation_result), ENSURE_SUCCESS(computation_result, return_code), etc.).

C ++ propose des outils supplémentaires sur C :

  • La fonctionnalité de bloc de nettoyage peut être implémentée en tant que RAII, ce qui signifie que vous n'avez plus besoin du cleanupbloc entier et que le code client peut ajouter des instructions de retour anticipées.

  • Vous lancez chaque fois que vous ne pouvez pas continuer, transformant tout cela if(error_ok != ...en appels simples.

Code C ++ équivalent:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

C'est la meilleure pratique car:

  • Il est explicite (c'est-à-dire que si la gestion des erreurs n'est pas explicite, le flux principal de l'algorithme est)

  • Il est simple d'écrire du code client

  • C'est minime

  • C'est simple

  • Il n'a pas de constructions de code répétitives

  • Il n'utilise pas de macros

  • Il n'utilise pas de do { ... } while(0)constructions étranges

  • Il est réutilisable avec un minimum d'effort (c'est-à-dire, si je veux copier l'appel vers computation2();une fonction différente, je n'ai pas à m'assurer d'ajouter un do { ... } while(0)dans le nouveau code, ni #defineune macro wrapper goto et une étiquette de nettoyage, ni rien d'autre).


+1. C'est ce que j'essaie d'utiliser, en utilisant RAII ou quelque chose. shared_ptravec un suppresseur personnalisé peut faire beaucoup de choses. Encore plus facile avec lambdas en C ++ 11.
Macke

Vrai, mais si vous finissez par utiliser shared_ptr pour des choses qui ne sont pas des pointeurs, pensez au moins à les taper: namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };dans ce cas (en make_handledéfinissant le type de suppression correct lors de la construction), le nom du type ne suggère plus qu'il s'agit d'un pointeur .
utnapistim

21

J'ajoute une réponse par souci d'exhaustivité. Un certain nombre d'autres réponses ont souligné que le grand bloc de conditions pouvait être divisé en une fonction distincte. Mais comme cela a également été souligné à plusieurs reprises, cette approche sépare le code conditionnel du contexte d'origine. C'est une des raisons pour lesquelles les lambdas ont été ajoutés au langage en C ++ 11. L'utilisation de lambdas a été suggérée par d'autres, mais aucun échantillon explicite n'a été fourni. J'en ai mis un dans cette réponse. Ce qui me frappe, c'est que cela ressemble beaucoup à l' do { } while(0)approche à bien des égards - et cela signifie peut-être que c'est toujours gotodéguisé ...

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations

7
Pour moi, ce hack semble pire que le faire ... tout en hack.
Michael

18

Certainement pas la réponse, mais une réponse (par souci d'exhaustivité)

Au lieu de :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Vous pourriez écrire:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

C'est toujours un goto déguisé, mais au moins ce n'est plus une boucle. Ce qui signifie que vous n'aurez pas à vérifier très attentivement qu'il n'y a pas de poursuite cachée quelque part dans le bloc.

La construction est également assez simple pour que vous puissiez espérer que le compilateur l'optimisera.

Comme suggéré par @jamesdlin, vous pouvez même masquer cela derrière une macro comme

#define BLOC switch(0) case 0:

Et utilisez-le comme

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Cela est possible car la syntaxe du langage C attend une instruction après un commutateur, pas un bloc entre crochets et vous pouvez mettre une étiquette de casse avant cette instruction. Jusqu'à présent, je ne voyais pas l'intérêt de permettre cela, mais dans ce cas particulier, il est pratique de cacher le commutateur derrière une belle macro.


2
Ooh, intelligent. Vous pouvez même le cacher derrière une macro comme define BLOCK switch (0) case 0:et l'utiliser comme BLOCK { ... break; }.
jamesdlin

@jamesdlin: il ne m'est jamais venu à l'esprit qu'il pourrait être utile de placer un boîtier avant l'ouverture du crochet d'un commutateur. Mais c'est en effet permis par C et dans ce cas c'est pratique d'écrire une belle macro.
Kriss du

15

Je recommanderais une approche similaire à la réponse de Mats moins l'inutile goto. Ne mettez que la logique conditionnelle dans la fonction. Tout code qui s'exécute toujours doit aller avant ou après l'appel de la fonction dans l'appelant:

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}

3
Si vous devez acquérir une autre ressource au milieu de func, vous devez factoriser une autre fonction (selon votre modèle). Si toutes ces fonctions isolées ont besoin des mêmes données, vous finirez par copier les mêmes arguments de pile encore et encore, ou décider d'allouer vos arguments sur le tas et de passer un pointeur, sans tirer parti de la fonctionnalité la plus élémentaire du langage ( arguments de fonction). En bref, je ne crois pas que cette solution s'adapte au pire des cas où chaque condition est vérifiée après l'acquisition de nouvelles ressources qui doivent être nettoyées. Notez cependant le commentaire de Nawaz sur les lambdas.
TNE

2
Je ne me souviens pas que le PO ait dit quoi que ce soit sur l'acquisition de ressources au milieu de son bloc de code, mais j'accepte cela comme une exigence réalisable. Dans ce cas, quel est le problème de déclarer la ressource sur la pile n'importe où func()et de permettre à son destructeur de gérer la libération des ressources? Si quelque chose en dehors de func()nécessite un accès à la même ressource, il doit être déclaré sur le tas avant d'être appelé func()par un gestionnaire de ressources approprié.
Dan Bechard

12

Le flux de code lui-même est déjà une odeur de code qui se produit trop dans la fonction. S'il n'y a pas de solution directe à cela (la fonction est une fonction de vérification générale), alors utiliser RAII pour pouvoir revenir au lieu de sauter à la fin d'une section de la fonction pourrait être mieux.


11

Si vous n'avez pas besoin d'introduire de variables locales pendant l'exécution, vous pouvez souvent aplatir ceci:

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()

2
Mais alors, chaque condition devrait inclure la précédente, qui est non seulement laide mais potentiellement mauvaise pour les performances. Mieux vaut sauter par-dessus ces vérifications et nettoyer immédiatement dès que nous savons que nous ne sommes "pas OK".
TNE

C'est vrai, cependant cette approche a des avantages s'il y a des choses à faire à la fin, donc vous ne voulez pas revenir tôt (voir éditer). Si vous combinez avec l'approche de Denise d'utiliser des bools dans les conditions, alors les performances seront négligeables à moins que ce ne soit dans une boucle très serrée.
the_mandrill

10

Similaire à la réponse de dasblinkenlight, mais évite l'affectation à l'intérieur du ifqui pourrait être «corrigée» par un réviseur de code:

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

J'utilise ce modèle lorsque les résultats d'une étape doivent être vérifiés avant l'étape suivante, ce qui diffère d'une situation où toutes les vérifications pourraient être effectuées à l'avance avec un if( check1() && check2()...modèle de grand type.


Notez que check1 () pourrait vraiment être PerformStep1 () qui retourne un code de résultat de l'étape. Cela réduirait la complexité de votre fonction de flux de processus.
Denise Skidmore

10

Utilisez des exceptions. Votre code sera beaucoup plus propre (et des exceptions ont été créées exactement pour gérer les erreurs dans le flux d'exécution d'un programme). Pour nettoyer les ressources (descripteurs de fichiers, connexions aux bases de données, etc.), lisez l'article Pourquoi C ++ ne fournit-il pas une construction "enfin"? .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}

10
Vraiment? Tout d'abord, je ne vois honnêtement aucune amélioration de la lisibilité, là-bas. N'UTILISEZ PAS D'EXCEPTIONS POUR CONTRÔLER LE DÉBIT DU PROGRAMME. Ce n'est PAS leur objectif. De plus, les exceptions entraînent des pénalités de performances importantes. Le cas d'utilisation approprié pour une exception est lorsqu'une condition est présente QUE VOUS NE POUVEZ PAS FAIRE QUOI QUE CE SOIT, comme essayer d'ouvrir un fichier qui est déjà exclusivement verrouillé par un autre processus, ou une connexion réseau échoue, ou un appel à une base de données échoue ou un appelant transmet un paramètre non valide à une procédure. Ce genre de chose. Mais N'UTILISEZ PAS d'exceptions pour contrôler le déroulement du programme.
Craig

3
Je veux dire, pour ne pas être une morosité à ce sujet, mais allez à cet article Stroustrup que vous avez référencé et recherchez le "Pour quoi ne devrais-je pas utiliser d'exceptions?" section. Entre autres choses, il dit: "En particulier, throw n'est pas simplement un moyen alternatif de retourner une valeur à partir d'une fonction (similaire à return). Cela sera lent et confondra la plupart des programmeurs C ++ habitués à voir les exceptions utilisées uniquement pour l'erreur De même, le lancer n'est pas un bon moyen de sortir d'une boucle. "
Craig

3
@Craig Tout ce que vous avez indiqué est correct, mais vous supposez que l'exemple de programme peut continuer après l' check()échec d' une condition, et c'est certainement VOTRE hypothèse, il n'y a pas de contexte dans l'exemple. En supposant que le programme ne peut pas continuer, utiliser des exceptions est la voie à suivre.
Cartucho

2
Eh bien, c'est vrai si tel est le contexte. Mais, chose drôle au sujet des hypothèses ... ;-)
Craig

10

Pour moi, do{...}while(0)c'est bien. Si vous ne voulez pas voir le do{...}while(0), vous pouvez leur définir des mots-clés alternatifs.

Exemple:

SomeUtilities.hpp:

#define BEGIN_TEST do{
#define END_TEST }while(0);

SomeSourceFile.cpp:

BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;
   
   //processing code here

END_TEST

Je pense que le compilateur supprimera la while(0)condition inutile do{...}while(0)dans la version binaire et convertira les ruptures en saut inconditionnel. Vous pouvez vérifier sa version en langage assembleur pour en être sûr.

L'utilisation gotoproduit également un code plus propre et c'est simple avec la logique condition-alors-saut. Vous pouvez effectuer les opérations suivantes:

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;
   
   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.
 

Notez que l'étiquette est placée après la fermeture }. Il s'agit d'éviter un problème possible en gotoplaçant accidentellement un code entre les deux parce que vous n'avez pas vu l'étiquette. C'est maintenant comme do{...}while(0)sans code de condition.

Pour rendre ce code plus propre et plus compréhensible, vous pouvez le faire:

SomeUtilities.hpp:

#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

SomeSourceFile.cpp:

BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

Avec cela, vous pouvez faire des blocs imbriqués et spécifier où vous souhaitez quitter / sauter.

BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);
  
      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

Le code spaghetti n'est pas la faute de goto, c'est la faute du programmeur. Vous pouvez toujours produire du code spaghetti sans utiliser goto.


10
Je choisirais gotoplutôt d'étendre la syntaxe du langage en utilisant le préprocesseur un million de fois.
Christian

2
"Pour rendre ce code plus propre et plus compréhensible, vous pouvez [utiliser LOADS_OF_WEIRD_MACROS]" : ne calcule pas.
underscore_d

noter sur la première phrase que la normale do{...}while(0) est préférée. cette réponse n'est qu'une suggestion et un jeu sur macro / goto / label pour éclater sur des blocs spécifiques.
acegs Il y a

8

C'est un problème bien connu et bien résolu du point de vue de la programmation fonctionnelle - peut-être la monade.

En réponse au commentaire que j'ai reçu ci-dessous, j'ai édité mon introduction ici: Vous pouvez trouver tous les détails sur la mise en œuvre des monades C ++ dans divers endroits qui vous permettront de réaliser ce que Rotsor suggère. Il faut du temps pour grogner des monades, donc je vais suggérer ici un mécanisme de type monade "pauvre" pour lequel vous n'avez besoin de rien savoir de plus que boost :: optional.

Configurez vos étapes de calcul comme suit:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

Chaque étape de calcul peut évidemment faire quelque chose comme return boost::nonesi l'option facultative qui lui a été donnée est vide. Ainsi, par exemple:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

Ensuite, enchaînez-les:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

La bonne chose à ce sujet est que vous pouvez écrire des tests unitaires clairement définis pour chaque étape de calcul. L'invocation se lit également comme un anglais simple (comme c'est généralement le cas avec le style fonctionnel).

Si vous ne vous souciez pas de l'immuabilité et qu'il est plus pratique de renvoyer le même objet à chaque fois que vous pouvez proposer une variation en utilisant shared_ptr ou similaire.


3
Ce code a une propriété indésirable de forcer chacune des fonctions individuelles à gérer l'échec de la fonction précédente, n'utilisant donc pas correctement l'idiome Monad (où les effets monadiques, dans ce cas, l'échec, sont censés être traités implicitement). Pour ce faire, vous devez avoir à la optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);place et utiliser l'opération de composition monadique ('bind') plutôt que l'application de fonction.
Rotsor

Merci. Vous avez raison - c'est la façon de le faire correctement. Je ne voulais pas trop écrire dans ma réponse pour expliquer cela (d'où le terme "pauvres-mans" qui devait suggérer que je n'allais pas tout le porc ici).
Benoît

7

Que diriez-vous de déplacer les instructions if dans une fonction supplémentaire produisant un résultat numérique ou enum?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}

C'est bien quand c'est possible, mais beaucoup moins général que la question posée ici. Chaque condition pourrait dépendre du code exécuté après le dernier test de déramification.
kriss

Le problème ici est que vous séparez la cause et la conséquence. C'est-à-dire que vous séparez le code qui fait référence au même numéro de condition et cela peut être une source de bugs supplémentaires.
Riga

@kriss: Eh bien, la fonction ConditionCode () pourrait être ajustée pour s'en occuper. Le point clé de cette fonction est que vous pouvez utiliser return <result> pour une sortie propre dès qu'une condition finale a été calculée; Et c'est ce qui apporte ici une clarté structurelle.
karx11erx

@Riga: Imo, ce sont des objections complètement académiques. Je trouve que le C ++ devient plus complexe, cryptique et illisible avec chaque nouvelle version. Je ne vois pas de problème avec une petite fonction d'aide évaluant des conditions complexes de manière bien structurée pour rendre la fonction cible juste en dessous plus lisible.
karx11erx

1
@ karx11erx mes objections sont pratiques et basées sur mon expérience. Ce modèle est mauvais sans rapport avec C ++ 11 ou n'importe quel langage. Si vous avez des difficultés avec les constructions de langage qui vous permettent d'écrire une bonne architecture, ce n'est pas un problème de langue.
Riga

5

Quelque chose comme ça peut-être

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

ou utiliser des exceptions

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

En utilisant des exceptions, vous pouvez également transmettre des données.


10
Veuillez ne pas faire de choses intelligentes comme votre définition de jamais, elles rendent généralement le code plus difficile à lire pour les autres développeurs. J'ai vu quelqu'un définir Case comme break; case dans un fichier d'en-tête et l'ai utilisé dans un commutateur dans un fichier cpp, ce qui fait que les autres se demandent pendant des heures pourquoi le commutateur se casse entre les instructions Case. Grrr ...
Michael

5
Et lorsque vous nommez des macros, vous devez les faire ressembler à des macros (c'est-à-dire en majuscules). Sinon, quelqu'un qui arrive à nommer une variable / fonction / type / etc. nommé eversera très malheureux ...
jamesdlin

5

Je ne suis pas particulièrement intéressé par l'utilisation breakoureturn dans un tel cas. Étant donné que normalement, lorsque nous sommes confrontés à une telle situation, il s'agit généralement d'une méthode relativement longue.

Si nous avons plusieurs points de sortie, cela peut causer des difficultés lorsque nous voulons savoir ce qui entraînera l'exécution de certaines logiques: normalement, nous continuons à monter des blocs enfermant ce morceau de logique, et les critères de ces blocs renfermant nous indiquent la situation:

Par exemple,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

En regardant les blocs englobants, il est facile de découvrir que cela myLogic()ne se produit que lorsque conditionA and conditionB and conditionCc'est vrai.

Cela devient beaucoup moins visible lorsqu'il y a des retours précoces:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

Nous ne pouvons plus naviguer vers le haut myLogic(), en regardant le bloc englobant pour comprendre la condition.

Il existe différentes solutions de contournement que j'ai utilisées. Voici l'un d'entre eux:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(Bien sûr, il est recommandé d'utiliser la même variable pour remplacer tous les isA isB isC .)

Une telle approche donnera au moins au lecteur de code, qui myLogic()est exécuté quand isB && conditionC. Le lecteur reçoit un indice qu'il a besoin de rechercher plus loin ce qui fera que isB soit vrai.


3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

Et ça?


le if est obsolète, juste return condition;, sinon je pense que c'est bien maintenable.
SpaceTrucker

2

Un autre modèle utile si vous avez besoin de différentes étapes de nettoyage en fonction de l'emplacement de l'échec:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }

2

Je ne suis pas un programmeur C ++ , donc je n'écrirai aucun code ici, mais jusqu'à présent, personne n'a mentionné de solution orientée objet. Voici donc ma supposition à ce sujet:

Avoir une interface générique qui fournit une méthode pour évaluer une seule condition. Vous pouvez maintenant utiliser une liste d'implémentations de ces conditions dans votre objet contenant la méthode en question. Vous parcourez la liste et évaluez chaque condition, pouvant éventuellement survenir tôt en cas d'échec.

La bonne chose est qu'une telle conception adhère très bien au principe ouvert / fermé , car vous pouvez facilement ajouter de nouvelles conditions lors de l'initialisation de l'objet contenant la méthode en question. Vous pouvez même ajouter une deuxième méthode à l'interface avec la méthode d'évaluation des conditions renvoyant une description de la condition. Cela peut être utilisé pour les systèmes d'auto-documentation.

L'inconvénient, cependant, est qu'il y a un peu plus de frais généraux impliqués en raison de l'utilisation de plus d'objets et de l'itération sur la liste.


Pourriez-vous ajouter un exemple dans une autre langue? Je pense que cette question s'applique à de nombreux langages bien qu'elle ait été posée spécifiquement sur C ++.
Denise Skidmore

1

C'est ainsi que je le fais.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}

1

Tout d'abord, un petit exemple pour montrer pourquoi ce goton'est pas une bonne solution pour C ++:

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

Essayez de compiler cela dans un fichier objet et voyez ce qui se passe. Essayez ensuite l'équivalent do+ break+while(0) .

C'était un aparté. Le point principal suit.

Ces petits morceaux de code nécessitent souvent une sorte de nettoyage si la fonction entière échoue. Ces nettoyages veulent généralement se produire dans l' ordre inverse des morceaux eux-mêmes, lorsque vous "déroulez" le calcul partiellement terminé.

Une option pour obtenir cette sémantique est RAII ; voir la réponse de @ utnapistim. C ++ garantit que les destructeurs automatiques fonctionnent dans l'ordre inverse des constructeurs, ce qui fournit naturellement un "déroulement".

Mais cela nécessite beaucoup de classes RAII. Parfois, une option plus simple consiste simplement à utiliser la pile:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...etc. Ceci est facile à auditer, car il place le code "annuler" à côté du code "faire". Un audit facile est bon. Cela rend également le flux de contrôle très clair. C'est aussi un modèle utile pour C.

Cela peut nécessiter que les calcfonctions prennent beaucoup d'arguments, mais ce n'est généralement pas un problème si vos classes / structures ont une bonne cohésion. (C'est-à-dire que les choses qui appartiennent ensemble vivent dans un seul objet, de sorte que ces fonctions peuvent prendre des pointeurs ou des références à un petit nombre d'objets tout en faisant beaucoup de travail utile.)


Très facile de vérifier le chemin de nettoyage, mais peut-être pas si facile de tracer le chemin d'or. Mais dans l'ensemble, je pense que quelque chose comme ça encourage un modèle de nettoyage cohérent.
Denise Skidmore

0

Si votre code contient un long bloc d'instructions if..else if..else, vous pouvez essayer de réécrire le bloc entier à l'aide de Functorsou function pointers. Ce n'est peut-être pas toujours la bonne solution, mais c'est souvent le cas.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html


En principe, cela est possible (et pas mal), mais avec des objets fonction ou des pointeurs explicites, cela perturbe trop fortement le flux de code. OTOH l'utilisation, ici équivalente, de lambdas ou de fonctions nommées ordinaires est une bonne pratique, efficace et bien lue.
leftaroundabout

0

Je suis étonné du nombre de réponses différentes présentées ici. Mais, finalement, dans le code que je dois changer (c'est-à-dire supprimer ce do-while(0)hack ou quoi que ce soit), j'ai fait quelque chose de différent de toutes les réponses mentionnées ici et je ne comprends pas pourquoi personne ne pensait cela. Voici ce que j'ai fait:

Code initial:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

Maintenant:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

Donc, ce qui a été fait ici, c'est que la finition a été isolée dans une fonction et que les choses sont soudainement devenues si simples et propres!

Je pensais que cette solution méritait d'être mentionnée, alors je l'ai fournie ici.


0

Consolidez-le en une seule ifdéclaration:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

Il s'agit du modèle utilisé dans des langages tels que Java où le mot-clé goto a été supprimé.


2
Cela ne fonctionne que si vous n'avez rien à faire entre les tests de condition. (eh bien, je suppose que vous pourriez cacher les choses à faire dans certains appels de fonction effectués par les tests de condition, mais cela pourrait être un peu obscurci si vous le faisiez trop)
Jeremy Friesner

@JeremyFriesner En fait, vous pouvez réellement faire les choses intermédiaires comme des fonctions booléennes distinctes qui sont toujours évaluées comme true. L'évaluation de court-circuit garantirait que vous ne courriez jamais entre des choses pour lesquelles tous les tests préalables n'ont pas réussi.
AJMansfield

@AJMansfield oui, c'est à cela que je faisais référence dans ma deuxième phrase ... mais je ne suis pas sûr que ce serait une amélioration de la qualité du code.
Jeremy Friesner

@JeremyFriesner Rien ne vous empêche d'écrire les conditions (/*do other stuff*/, /*next condition*/), vous pouvez même les formater correctement. Ne vous attendez pas à ce que les gens l'aiment. Mais honnêtement, cela ne fait que montrer que c'était une erreur pour Java de supprimer cette gotodéclaration ...
cmaster - réintégrer monica

@JeremyFriesner Je supposais qu'il s'agissait de booléens. Si des fonctions devaient être exécutées à l'intérieur de chaque condition, il existe une meilleure façon de les gérer.
Tyzoid

0

Si vous utilisez le même gestionnaire d'erreurs pour toutes les erreurs et que chaque étape renvoie un booléen indiquant la réussite:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(Similaire à la réponse de tyzoid, mais les conditions sont les actions, et le && empêche des actions supplémentaires de se produire après le premier échec.)


0

Pourquoi n'a-t-on pas répondu à la méthode de signalisation, elle est utilisée depuis des siècles.

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.