Pourquoi utiliser des instructions do-while et if-else apparemment dénuées de sens dans les macros?

788

Dans de nombreuses macros C / C ++, je vois le code de la macro enveloppé dans ce qui semble être une do whileboucle vide de sens . Voici des exemples.

#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else

Je ne vois pas ce que ça do whilefait. Pourquoi ne pas simplement écrire cela sans cela?

#define FOO(X) f(X); g(X)
jfm3
la source
2
Pour l'exemple avec le else, j'ajouterais une expression de voidtype à la fin ... comme ((void) 0) .
Phil1970
1
Rappelez-vous que la do whileconstruction n'est pas compatible avec les instructions de retour, donc la if (1) { ... } else ((void)0)construction a des usages plus compatibles dans la norme C. Et dans GNU C, vous préférerez la construction décrite dans ma réponse.
Cœur

Réponses:

829

Le do ... whileet if ... elsesont là pour faire en sorte qu'un point-virgule après votre macro signifie toujours la même chose. Disons que vous aviez quelque chose comme votre deuxième macro.

#define BAR(X) f(x); g(x)

Maintenant, si vous deviez utiliser BAR(X);dans une if ... elseinstruction, où les corps de l'instruction if n'étaient pas placés entre crochets, vous obtiendriez une mauvaise surprise.

if (corge)
  BAR(corge);
else
  gralt();

Le code ci-dessus se développerait en

if (corge)
  f(corge); g(corge);
else
  gralt();

ce qui est syntaxiquement incorrect, car le reste n'est plus associé à l'if. Cela n'aide pas à encapsuler les choses entre accolades dans la macro, car un point-virgule après les accolades est syntaxiquement incorrect.

if (corge)
  {f(corge); g(corge);};
else
  gralt();

Il existe deux façons de résoudre le problème. La première consiste à utiliser une virgule pour séquencer les instructions au sein de la macro sans la priver de sa capacité à agir comme une expression.

#define BAR(X) f(X), g(X)

La version ci-dessus de bar BARdéveloppe le code ci-dessus dans ce qui suit, ce qui est syntaxiquement correct.

if (corge)
  f(corge), g(corge);
else
  gralt();

Cela ne fonctionne pas si, au lieu de cela, f(X)vous avez un corps de code plus compliqué qui doit aller dans son propre bloc, par exemple pour déclarer des variables locales. Dans le cas le plus général, la solution consiste à utiliser quelque chose comme do ... whilepour que la macro soit une instruction unique qui prend un point-virgule sans confusion.

#define BAR(X) do { \
  int i = f(X); \
  if (i > 4) g(i); \
} while (0)

Vous n'avez pas besoin de l'utiliser do ... while, vous pouvez également préparer quelque chose avec if ... else, bien que quand il se if ... elsedéveloppe à l'intérieur d'un, if ... elsecela mène à un " balancement d'autre ", ce qui pourrait rendre un problème de balancement d'autre encore plus difficile à trouver, comme dans le code suivant .

if (corge)
  if (1) { f(corge); g(corge); } else;
else
  gralt();

Le point est d'utiliser le point-virgule dans des contextes où un point-virgule pendant est erroné. Bien sûr, on pourrait (et devrait probablement) faire valoir à ce stade qu’il serait préférable de déclarerBAR comme une fonction réelle, et non comme une macro.

En résumé, le do ... whileest là pour contourner les lacunes du préprocesseur C. Lorsque ces guides de style C vous disent de mettre à pied le préprocesseur C, c'est le genre de chose qui les inquiète.

jfm3
la source
18
N'est-ce pas un argument fort pour toujours utiliser des accolades dans les instructions if, while et for? Si vous vous efforcez de toujours faire cela (comme cela est requis pour MISRA-C, par exemple), le problème décrit ci-dessus disparaît.
Steve Melnikoff
17
L'exemple de virgule doit être #define BAR(X) (f(X), g(X))sinon la priorité de l'opérateur peut perturber la sémantique.
Stewart
20
@DawidFerenczy: bien que vous et moi, il y a quatre ans et demi, ayez raison, nous devons vivre dans le monde réel. À moins que nous ne puissions garantir que toutes les ifinstructions, etc., dans notre code utilisent des accolades, alors encapsuler des macros comme celle-ci est un moyen simple d'éviter les problèmes.
Steve Melnikoff
8
Remarque: le if(1) {...} else void(0)formulaire est plus sûr que do {...} while(0)pour les macros dont les paramètres sont du code inclus dans l'expansion de macro, car il ne modifie pas le comportement des mots-clés break ou continue. Par exemple: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }provoque un comportement inattendu lorsque MYMACROest défini comme #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)parce que la rupture affecte la boucle while de la macro plutôt que la boucle for sur le site d'appel de macro.
Chris Kline
5
@ace void(0)était une faute de frappe, je veux dire (void)0. Et je crois que cela ne résout le problème « ballants autre »: avis il n'y a pas après la virgule (void)0. Un autre pendant dans ce cas (par exemple if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) déclenche une erreur de compilation. Voici un exemple en direct le démontrant
Chris Kline
153

Les macros sont des morceaux de texte copiés / collés que le pré-processeur mettra dans le code authentique; l'auteur de la macro espère que le remplacement produira un code valide.

Il y a trois bons "conseils" pour y parvenir:

Aidez la macro à se comporter comme un code authentique

Le code normal se termine généralement par un point-virgule. Si l'utilisateur affiche le code qui n'en a pas besoin ...

doSomething(1) ;
DO_SOMETHING_ELSE(2)  // <== Hey? What's this?
doSomethingElseAgain(3) ;

Cela signifie que l'utilisateur s'attend à ce que le compilateur produise une erreur si le point-virgule est absent.

Mais la vraie vraie bonne raison est qu'à un moment donné, l'auteur de la macro devra peut-être remplacer la macro par une véritable fonction (peut-être en ligne). Donc, la macro devrait vraiment se comporter comme telle.

Nous devrions donc avoir une macro nécessitant un point-virgule.

Produire un code valide

Comme indiqué dans la réponse de jfm3, parfois la macro contient plus d'une instruction. Et si la macro est utilisée dans une instruction if, ce sera problématique:

if(bIsOk)
   MY_MACRO(42) ;

Cette macro pourrait être étendue comme suit:

#define MY_MACRO(x) f(x) ; g(x)

if(bIsOk)
   f(42) ; g(42) ; // was MY_MACRO(42) ;

La gfonction sera exécutée quelle que soit la valeur debIsOk .

Cela signifie que nous devons avoir à ajouter une portée à la macro:

#define MY_MACRO(x) { f(x) ; g(x) ; }

if(bIsOk)
   { f(42) ; g(42) ; } ; // was MY_MACRO(42) ;

Produire un code valide 2

Si la macro est quelque chose comme:

#define MY_MACRO(x) int i = x + 1 ; f(i) ;

Nous pourrions avoir un autre problème dans le code suivant:

void doSomething()
{
    int i = 25 ;
    MY_MACRO(32) ;
}

Parce qu'il s'élargirait comme:

void doSomething()
{
    int i = 25 ;
    int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
}

Ce code ne se compilera pas, bien sûr. Donc, encore une fois, la solution utilise une portée:

#define MY_MACRO(x) { int i = x + 1 ; f(i) ; }

void doSomething()
{
    int i = 25 ;
    { int i = 32 + 1 ; f(i) ; } ; // was MY_MACRO(32) ;
}

Le code se comporte à nouveau correctement.

Combiner les effets point-virgule + scope?

Il existe un idiome C / C ++ qui produit cet effet: La boucle do / while:

do
{
    // code
}
while(false) ;

Le do / while peut créer une portée, encapsulant ainsi le code de la macro, et a besoin d'un point-virgule à la fin, se développant ainsi dans le code qui en a besoin.

Le bonus?

Le compilateur C ++ optimisera la boucle do / while, car le fait que sa post-condition soit fausse est connu au moment de la compilation. Cela signifie qu'une macro comme:

#define MY_MACRO(x)                                  \
do                                                   \
{                                                    \
    const int i = x + 1 ;                            \
    f(i) ; g(i) ;                                    \
}                                                    \
while(false)

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      MY_MACRO(42) ;

   // Etc.
}

se développera correctement

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      do
      {
         const int i = 42 + 1 ; // was MY_MACRO(42) ;
         f(i) ; g(i) ;
      }
      while(false) ;

   // Etc.
}

et est ensuite compilé et optimisé comme

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
   {
      f(43) ; g(43) ;
   }

   // Etc.
}
paercebal
la source
6
Notez que la modification des macros en fonction en ligne modifie certaines macros prédéfinies standard, par exemple le code suivant montre un changement dans FUNCTION et LINE : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", FUNCTION , LINE ); } int main () {Fmacro (); Finline (); retourner 0; } (les termes en gras doivent être entourés de doubles soulignements - mauvais formateur!)
Gnubie
6
Il y a un certain nombre de problèmes mineurs mais pas complètement sans conséquence avec cette réponse. Par exemple: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }n'est pas la bonne extension; l' xexpansion devrait être de 32. Une question plus complexe est celle de l'expansion de MY_MACRO(i+7). Et une autre est l'expansion de MY_MACRO(0x07 << 6). Il y a beaucoup de bonnes choses, mais il y a des i non pointés et des t non croisés.
Jonathan Leffler
@Gnubie: Si vous êtes toujours là et que vous ne l'avez pas encore compris: vous pouvez échapper les astérisques et les traits de soulignement dans les commentaires avec des barres obliques inverses, donc si vous tapez, \_\_LINE\_\_le rendu est __LINE__. À mon humble avis, il est préférable d'utiliser simplement la mise en forme du code pour le code; par exemple, __LINE__(qui ne nécessite aucune manipulation particulière). PS Je ne sais pas si c'était vrai en 2012; ils ont depuis apporté quelques améliorations au moteur.
Scott
1
Appréciant que mon commentaire ait six ans de retard, mais la plupart des compilateurs C n'incluent pas réellement les inlinefonctions (comme le permet la norme)
Andrew
53

@ jfm3 - Vous avez une belle réponse à la question. Vous voudrez peut-être également ajouter que l'idiome de macro empêche également le comportement involontaire éventuellement plus dangereux (car il n'y a pas d'erreur) avec de simples instructions «si»:

#define FOO(x)  f(x); g(x)

if (test) FOO( baz);

s'étend à:

if (test) f(baz); g(baz);

qui est syntaxiquement correct donc il n'y a pas d'erreur de compilation, mais a la conséquence probablement non voulue que g () sera toujours appelé.

Michael Burr
la source
22
"probablement involontaire"? J'aurais dit "certainement involontaire", sinon le programmeur doit être sorti et abattu (par opposition à un châtiment brutal avec un fouet).
Lawrence Dol
4
Ou ils pourraient recevoir une augmentation, s'ils travaillent pour une agence de trois lettres et insèrent subrepticement ce code dans un programme open source largement utilisé ... :-)
R .. GitHub STOP HELPING ICE
2
Et ce commentaire me rappelle juste la ligne d'échec goto dans le récent bogue de vérification de certificat SSL trouvé dans les systèmes d'exploitation Apple
Gerard Sexton
23

Les réponses ci-dessus expliquent la signification de ces constructions, mais il existe une différence significative entre les deux qui n'a pas été mentionnée. En fait, il y a une raison de préférer le do ... whileà la if ... elseconstruction.

Le problème de la if ... elseconstruction est qu'elle ne vous oblige pas à mettre le point-virgule. Comme dans ce code:

FOO(1)
printf("abc");

Bien que nous ayons omis le point-virgule (par erreur), le code sera étendu à

if (1) { f(X); g(X); } else
printf("abc");

et compilera en silence (bien que certains compilateurs puissent émettre un avertissement pour le code inaccessible). Mais la printfdéclaration ne sera jamais exécutée.

do ... whileLa construction n'a pas un tel problème, car le seul jeton valide après le while(0)est un point-virgule.

Yakov Galka
la source
2
@RichardHansen: Toujours pas aussi bien, car à la lecture de l'invocation de macro, vous ne savez pas si elle se développe en une instruction ou en une expression. Si quelqu'un assume la dernière, elle peut écrire FOO(1),x++;ce qui nous donnera à nouveau un faux positif. Utilisez-le do ... whileet c'est tout.
Yakov Galka
1
Documenter la macro pour éviter les malentendus devrait suffire. Je suis d'accord que do ... while (0)c'est préférable, mais il a un inconvénient: A breakou continuecontrôlera la do ... while (0)boucle, pas la boucle contenant l'invocation de macro. L' ifastuce a donc toujours de la valeur.
Richard Hansen
2
Je ne vois pas où vous pourriez mettre un breakou un continuequi serait vu comme dans votre do {...} while(0)pseudo-boucle de macros . Même dans le paramètre macro, cela ferait une erreur de syntaxe.
Patrick Schlüter
5
Une autre raison d'utiliser do { ... } while(0)au lieu de if whateverconstruire, est sa nature idiomatique. La do {...} while(0)construction est répandue, bien connue et beaucoup utilisée par de nombreux programmeurs. Sa justification et sa documentation sont facilement connues. Ce n'est pas le cas pour la ifconstruction. Il faut donc moins d'efforts pour grogner lors de la révision de code.
Patrick Schlüter
1
@tristopia: J'ai vu des gens écrire des macros qui prennent des blocs de code comme arguments (ce que je ne recommande pas nécessairement). Par exemple: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Il pourrait être utilisé comme CHECK(system("foo"), break;);, où le break;est destiné à faire référence à la boucle entourant l' CHECK()invocation.
Richard Hansen
16

Bien qu'il soit prévu que les compilateurs optimisent les do { ... } while(false);boucles, il existe une autre solution qui ne nécessiterait pas cette construction. La solution est d'utiliser l'opérateur virgule:

#define FOO(X) (f(X),g(X))

ou encore plus exotiquement:

#define FOO(X) g((f(X),(X)))

Bien que cela fonctionne bien avec des instructions distinctes, cela ne fonctionnera pas avec les cas où les variables sont construites et utilisées dans le cadre de #define:

#define FOO(X) (int s=5,f((X)+s),g((X)+s))

Avec cela, on serait obligé d'utiliser la construction do / while.

Marius
la source
merci, puisque l'opérateur virgule ne garantit pas l'ordre d'exécution, cette imbrication est un moyen de faire respecter cela.
Marius
15
@Marius: False; l'opérateur virgule est un point de séquence et de ce fait ne garantit ordre d'exécution. Je soupçonne que vous l'avez confondu avec la virgule dans les listes d'arguments de fonction.
R .. GitHub STOP HELPING ICE
2
La deuxième suggestion exotique a fait ma journée.
Spidey
Je voulais juste ajouter que les compilateurs sont obligés de conserver le comportement observable du programme, donc l'optimisation du do / while away n'est pas très grave (en supposant que les optimisations du compilateur sont correctes).
Marco A.
@MarcoA. bien que vous ayez raison, j'ai trouvé dans le passé que l'optimisation du compilateur, tout en préservant exactement la fonction du code, mais en changeant de ligne qui ne semble rien faire dans le contexte singulier, briserait les algorithmes multithread. Affaire au point Peterson's Algorithm.
Marius
11

La bibliothèque de préprocesseurs P99 de Jens Gustedt (oui, le fait qu'une telle chose existe m'a aussi fait réfléchir !) Améliore la if(1) { ... } elseconstruction de manière petite mais significative en définissant ce qui suit:

#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP

La raison en est que, contrairement à la do { ... } while(0)construction, breaket continuefonctionne toujours à l'intérieur du bloc donné, mais ((void)0)crée une erreur de syntaxe si le point-virgule est omis après l'appel de la macro, ce qui autrement ignorerait le bloc suivant. (Il n'y a pas vraiment de problème de "balançoire d'autre" ici, car le elselien au plus procheif , qui est celui de la macro.)

Si vous êtes intéressé par le genre de choses qui peuvent être faites plus ou moins en toute sécurité avec le préprocesseur C, consultez cette bibliothèque.

Isaac Schwabacher
la source
Bien que très intelligent, cela provoque un bombardement d'avertissements du compilateur sur les risques de balancement.
Segmenté
1
Vous utilisez généralement des macros pour créer un environnement confiné, c'est-à-dire que vous n'utilisez jamais un break(ou continue) à l'intérieur d'une macro pour contrôler une boucle qui a commencé / s'est terminée à l'extérieur, c'est juste un mauvais style et cache des points de sortie potentiels.
mirabilos
Il existe également une bibliothèque de préprocesseurs dans Boost. Qu'est-ce qui est époustouflant?
René
Le risque else ((void)0)est que quelqu'un écrive YOUR_MACRO(), f();et qu'il soit syntaxiquement valide, mais n'appelle jamais f(). Avec do whilec'est une erreur de syntaxe.
melpomene
@melpomene alors qu'en est-il else do; while (0)?
Carl Lei
8

Pour certaines raisons, je ne peux pas commenter la première réponse ...

Certains d'entre vous ont montré des macros avec des variables locales, mais personne n'a mentionné que vous ne pouvez pas utiliser n'importe quel nom dans une macro! Il mordra l'utilisateur un jour! Pourquoi? Parce que les arguments d'entrée sont substitués dans votre modèle de macro. Et dans vos exemples de macro, vous avez utilisé le nom variable probablement le plus couramment utilisé i .

Par exemple, lorsque la macro suivante

#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)

est utilisé dans la fonction suivante

void some_func(void) {
    int i;
    for (i = 0; i < 10; ++i)
        FOO(i);
}

la macro n'utilisera pas la variable i prévue, qui est déclarée au début de some_func, mais la variable locale, qui est déclarée dans la boucle do ... while de la macro.

Ainsi, n'utilisez jamais de noms de variables communs dans une macro!

Mike Meyer
la source
Le schéma habituel consiste à ajouter des traits de soulignement dans les noms de variables dans les macros - par exemple int __i;.
Blaisorblade
8
@Blaisorblade: En fait, c'est C incorrect et illégal; les traits de soulignement principaux sont réservés à l'utilisation par l'implémentation. La raison pour laquelle vous avez vu ce "modèle habituel" vient de la lecture des en-têtes du système ("l'implémentation") qui doivent se limiter à cet espace de noms réservé. Pour les applications / bibliothèques, vous devez choisir vos propres noms obscurs, peu susceptibles de se heurter sans traits de soulignement, par exemple mylib_internal___iou similaires.
R .. GitHub STOP HELPING ICE
2
@R .. Vous avez raison - j'ai lu ceci dans une '' application '', le noyau Linux, mais c'est quand même une exception car il n'utilise pas de bibliothèque standard (techniquement, une implémentation C '' autonome '' à la place d'un '' hébergé '').
Blaisorblade
3
@R .. ce n'est pas tout à fait correct: les soulignements de tête suivis d'un majuscule ou d'un deuxième soulignement sont réservés à l'implémentation dans tous les contextes. Les tirets bas suivis par autre chose ne sont pas réservés dans la portée locale.
Leushenko
@Leushenko: Oui, mais la distinction est suffisamment subtile pour que je trouve préférable de dire aux gens de ne pas du tout utiliser de tels noms. Les gens qui comprennent la subtilité savent probablement déjà que je passe sous silence les détails. :-)
R .. GitHub STOP STOPINGING ICE
7

Explication

do {} while (0)et if (1) {} elsedoivent s'assurer que la macro est étendue à une seule instruction. Autrement:

if (something)
  FOO(X); 

s'étendrait à:

if (something)
  f(X); g(X); 

Et g(X)serait exécuté en dehors de la ifdéclaration de contrôle. Ceci est évité lors de l'utilisation de do {} while (0)et if (1) {} else.


Meilleure alternative

Avec une GNU expression de déclaration ( et non une partie de C standard), vous avez une meilleure façon que do {} while (0)et if (1) {} elsepour résoudre ce problème, en utilisant simplement ({}):

#define FOO(X) ({f(X); g(X);})

Et cette syntaxe est compatible avec les valeurs de retour (notez que ce do {} while (0)n'est pas le cas), comme dans:

return FOO("X");
Cœur
la source
3
l'utilisation du blocage de bloc {} dans la macro serait suffisante pour regrouper le code de macro afin que tout soit exécuté pour le même chemin de condition if. le do-while around est utilisé pour appliquer un point-virgule aux endroits où la macro est utilisée. ainsi, la macro est appliquée en se comportant de manière plus fonctionnelle. cela inclut l'exigence pour le point-virgule de fin lorsqu'il est utilisé.
Alexander Stohr
2

Je ne pense pas que cela ait été mentionné, alors considérez ceci

while(i<100)
  FOO(i++);

serait traduit en

while(i<100)
  do { f(i++); g(i++); } while (0)

remarquez comment i++est évalué deux fois par la macro. Cela peut conduire à des erreurs intéressantes.

John Nilsson
la source
15
Cela n'a rien à voir avec la construction do ... while (0).
Trent
2
Vrai. Mais pertinent pour le sujet des macros contre les fonctions et comment écrire une macro qui se comporte comme une fonction ...
John Nilsson
2
Similairement à la précédente, ce n'est pas une réponse mais un commentaire. Sur le sujet: c'est pourquoi vous n'utilisez des trucs qu'une seule fois:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos