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!
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 LONGdo ... while(0);
semble être la mauvaise chose.goto
, soyez honnête à ce sujet et faites-le en plein air, ne le cachez pas en utilisantbreak
etdo ... while
goto
effort de réhabilitation. :)Réponses:
Il est considéré comme une pratique acceptable d'isoler ces décisions dans une fonction et d'utiliser
return
s au lieu debreak
s. 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:
la source
return
est plus propre, car tout lecteur est immédiatement conscient qu'il fonctionne correctement et ce qu'il fait. Avecgoto
vous 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.forceinline
.Il y a des moments où utiliser
goto
est en fait la bonne réponse - au moins pour ceux qui ne sont pas élevés dans la croyance religieuse qui "goto
ne 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 ungoto
comme unbreak
. Si vous allez utilisergoto
, 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:
Cela peut en fait rendre plus clair que le code est correct en n'ayant pas une logique aussi complexe.
Edit: Pour clarifier, je ne propose en aucun cas l'utilisation de
goto
comme solution générale. Mais il y a des cas où ilgoto
y 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.
la source
goto
peut avoir une place, maisgoto cleanup
pas. Le nettoyage se fait avec RAII.goto
n'est jamais la bonne réponse. J'apprécierais s'il y avait un commentaire ...goto
parce que vous devez penser à l'utiliser / à comprendre un programme qui l'utilise ... Les microprocesseurs, d'autre part, sont construitsjumps
etconditional jumps
... donc le problème est avec certaines personnes, pas avec la logique ou autre chose.goto
. RAII n'est pas la bonne solution si des erreurs sont attendues (pas exceptionnelles), car ce serait un abus des exceptions.Vous pouvez utiliser un modèle de continuation simple avec une
bool
variable:Cette chaîne d'exécution s'arrêtera dès que
checkN
retourne afalse
. Aucun autrecheck...()
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églagegoOn
surfalse
est à sens unique et insérer les éléments manquantsgoto end
pour vous. En conséquence, les performances du code ci-dessus seraient identiques à celles d'undo
/while(0)
, sans un coup douloureux à sa lisibilité.la source
if
conditions semblent très suspectes.if
peu importe ce qui segoOn
passait 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 utilisezgoOn &= checkN()
alors,checkN()
il s'exécutera toujours même s'ilgoOn
étaitfalse
au début duif
(c.-à-d. ne le faites pas).if
s.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.
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
break
parreturn
(outhrow
, si cela est plus approprié) et laissez le destructeur de l'objet nettoyer pour vous.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.Si vous avez des règles de codage qui l'interdisent aveuglément
goto
et que vous ne pouvez vraiment pas simplifier le flux du programme, vous devrez probablement le masquer avec votredo
hack.la source
goto
si c'est vraiment une meilleure option.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:
En C, dans la plupart codebases, le
if(error_ok != ...
et legoto
code 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
cleanup
bloc 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:
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 étrangesIl 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 undo { ... } while(0)
dans le nouveau code, ni#define
une macro wrapper goto et une étiquette de nettoyage, ni rien d'autre).la source
shared_ptr
avec un suppresseur personnalisé peut faire beaucoup de choses. Encore plus facile avec lambdas en C ++ 11.namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };
dans ce cas (enmake_handle
dé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 .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 toujoursgoto
déguisé ...la source
Certainement pas la réponse, mais une réponse (par souci d'exhaustivité)
Au lieu de :
Vous pourriez écrire:
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
Et utilisez-le comme
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.
la source
define BLOCK switch (0) case 0:
et l'utiliser commeBLOCK { ... break; }
.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:la source
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.func()
et de permettre à son destructeur de gérer la libération des ressources? Si quelque chose en dehors defunc()
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é.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.
la source
Si vous n'avez pas besoin d'introduire de variables locales pendant l'exécution, vous pouvez souvent aplatir ceci:
la source
Similaire à la réponse de dasblinkenlight, mais évite l'affectation à l'intérieur du
if
qui pourrait être «corrigée» par un réviseur de code:...
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.la source
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"? .
la source
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.Pour moi,
do{...}while(0)
c'est bien. Si vous ne voulez pas voir ledo{...}while(0)
, vous pouvez leur définir des mots-clés alternatifs.Exemple:
SomeUtilities.hpp:
SomeSourceFile.cpp:
Je pense que le compilateur supprimera la
while(0)
condition inutiledo{...}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
goto
produit également un code plus propre et c'est simple avec la logique condition-alors-saut. Vous pouvez effectuer les opérations suivantes:Notez que l'étiquette est placée après la fermeture
}
. Il s'agit d'éviter un problème possible engoto
plaçant accidentellement un code entre les deux parce que vous n'avez pas vu l'étiquette. C'est maintenant commedo{...}while(0)
sans code de condition.Pour rendre ce code plus propre et plus compréhensible, vous pouvez le faire:
SomeUtilities.hpp:
SomeSourceFile.cpp:
Avec cela, vous pouvez faire des blocs imbriqués et spécifier où vous souhaitez quitter / sauter.
Le code spaghetti n'est pas la faute de
goto
, c'est la faute du programmeur. Vous pouvez toujours produire du code spaghetti sans utilisergoto
.la source
goto
plutôt d'étendre la syntaxe du langage en utilisant le préprocesseur un million de fois.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.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:
Chaque étape de calcul peut évidemment faire quelque chose comme return
boost::none
si l'option facultative qui lui a été donnée est vide. Ainsi, par exemple:Ensuite, enchaînez-les:
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.
la source
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.Que diriez-vous de déplacer les instructions if dans une fonction supplémentaire produisant un résultat numérique ou enum?
la source
Quelque chose comme ça peut-être
ou utiliser des exceptions
En utilisant des exceptions, vous pouvez également transmettre des données.
la source
ever
sera très malheureux ...Je ne suis pas particulièrement intéressé par l'utilisation
break
oureturn
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,
En regardant les blocs englobants, il est facile de découvrir que cela
myLogic()
ne se produit que lorsqueconditionA and conditionB and conditionC
c'est vrai.Cela devient beaucoup moins visible lorsqu'il y a des retours précoces:
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:
(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é quandisB && conditionC
. Le lecteur reçoit un indice qu'il a besoin de rechercher plus loin ce qui fera que isB soit vrai.la source
Et ça?
la source
return condition;
, sinon je pense que c'est bien maintenable.Un autre modèle utile si vous avez besoin de différentes étapes de nettoyage en fonction de l'emplacement de l'échec:
la source
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.
la source
C'est ainsi que je le fais.
la source
Tout d'abord, un petit exemple pour montrer pourquoi ce
goto
n'est pas une bonne solution pour C ++: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:
...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
calc
fonctions 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.)la source
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
Functors
oufunction 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
la source
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:
Maintenant:
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.
la source
Consolidez-le en une seule
if
déclaration:Il s'agit du modèle utilisé dans des langages tels que Java où le mot-clé goto a été supprimé.
la source
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.(/*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 cettegoto
déclaration ...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:
(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.)
la source
Pourquoi n'a-t-on pas répondu à la méthode de signalisation, elle est utilisée depuis des siècles.
la source