Pourquoi utiliser en macros des déclarations apparemment dénuées de sens de "Do-while" et de "if-else"?

dans beaucoup de macros C/C++ je vois le code de la macro enveloppé dans ce qui ressemble à une boucle do while sans signification. Ici sont 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 fait le do while . Pourquoi ne pas simplement écrire ceci sans elle?

#define FOO(X) f(X); g(X)
672
demandé sur Lundin 2008-09-30 21:36:24
la source

10 ответов

les do ... while et if ... else sont là pour faire en sorte qu'un point-virgule après votre macro signifie toujours la même chose. Disons que vous il y avait quelque chose comme ta deuxième macro.

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

maintenant si vous deviez utiliser BAR(X); dans une déclaration if ... else , où les corps de la déclaration si n'étaient pas enveloppés dans des crochets bouclés, vous obtiendriez une mauvaise surprise.

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

le code ci-dessus s'étendrait en

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

qui est syntaxiquement incorrect, car l'autre n'est plus associé avec le si. Il n'aide pas à envelopper les choses dans des accolades bouclées dans la macro, parce qu'un point-virgule après les accolades est syntaxiquement incorrect.

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

Il y a deux façons de régler le problème. La première consiste à utiliser une virgule pour séquencer les énoncés à l'intérieur 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 la barre BAR étend 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 f(X) vous avez un corps de code plus compliqué qui doit aller dans son propre bloc, disons par exemple déclarer des variables locales. Dans le cas le plus général, la solution est d'utiliser quelque chose comme do ... while pour faire en sorte que la macro soit une simple instruction qui prend un point-virgule Sans confusion.

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

vous ne devez pas utiliser do ... while , vous pourriez cuisiner quelque chose avec if ... else aussi, bien que lorsque if ... else se développe à l'intérieur d'un if ... else il conduit à un " pendaison autrement ", ce qui pourrait rendre un problème existant pendaison autrement 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 les contextes où un point-virgule est bancale erroné. Bien sûr, il pourrait (et devrait) être soutenu à ce point qu'il serait mieux de déclarer BAR comme une fonction réelle, pas une macro.

en résumé, le do ... while est là pour travailler autour des défauts du préprocesseur C. Lorsque les guides de style vous dire de licencier le préprocesseur C, c'est le genre de chose qu'il craint.

731
répondu jfm3 2014-06-15 22:26:38
la source
Les Macros

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

il y a trois bons" conseils "pour réussir en cela:

Aide de la macro se comportent comme de véritables code

Le code Normal

se termine habituellement par un point-virgule. Si le code d'affichage de l'utilisateur n'en a pas besoin...

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

l'utilisateur s'attend à ce que le compilateur génère une erreur si le point-virgule est absent.

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

donc nous devrions avoir un macro qui a besoin d'un semi-côlon.

produire un code valide

comme indiqué dans les réponse, parfois la macro contient plus d'une instruction. Et si la macro est utilisée à l'intérieur d'une instruction if, ce sera problématique:

if(bIsOk)
   MY_MACRO(42) ;

cette macro pourrait être étendue comme:

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

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

la fonction g sera exécutée quelle que soit la valeur de bIsOk .

cela signifie que nous devons 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'étendrait comme:

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

ce code ne sera pas compilé, 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.

Combinant semi-colon + effets de périmètre?

il y a 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 prolongeant 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 est false 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 en tant que

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.
}
136
répondu paercebal 2016-04-13 11:31:21
la source

@jfm3-vous avez une bonne réponse à la question. Vous pourriez aussi vouloir ajouter que le langage macro empêche aussi le comportement éventuellement plus dangereux (parce qu'il n'y a pas d'erreur) non intentionnel avec de simples " si "énoncés:

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

if (test) FOO( baz);

devient:

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

qui est syntaxiquement correct de sorte qu'il n'y a pas d'erreur de compilateur, mais qui a la conséquence probablement involontaire que g() sera toujours appelé.

50
répondu Michael Burr 2008-09-30 22:05:30
la source

les réponses ci-dessus expliquent le sens de ces concepts, mais il y a 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 au if ... else construction.

le problème de la construction if ... else est qu'il ne force vous de 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 sera compilé en silence (bien que certains compilateurs peuvent émettre un avertissement pour le code inaccessible). Mais la déclaration printf ne sera jamais exécutée.

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

21
répondu ybungalobill 2012-08-03 19:21:08
la source

bien qu'on s'attende à ce que les compilateurs optimisent les boucles do { ... } while(false); , il existe une autre solution qui n'exigerait 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 séparées, cela ne fonctionnera pas avec les cas où les variables sont construites et utilisées dans le cadre du #define :

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

avec celui-ci serait forcé d'utiliser la construction do/while.

15
répondu Marius 2009-10-10 12:33:06
la source

Jens Gustedt P99 préprocesseur bibliothèque (oui, le fait qu'une telle chose existe a soufflé mon esprit aussi!) améliore la construction if(1) { ... } else d'une 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 construction do { ... } while(0) , break et continue fonctionnent toujours à l'intérieur du bloc donné, mais le ((void)0) crée une erreur de syntaxe si le point-virgule est omis après le appel macro, qui sauterait le prochain bloc. (Il n'y a pas vraiment de problème de "pendaison d'autre" ici, puisque le else se lie au if le plus proche , qui est celui de la macro.)

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

8
répondu Isaac Schwabacher 2014-11-19 19:19:45
la source

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 va mordre à l'utilisateur un jour! Pourquoi? Parce que les arguments d'entrée sont substitués dans votre modèle macro. Et dans vos exemples macro vous avez utilisé le nom probablement le plus couramment utilisé variabled 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 voulue i, qui est déclarée au début de some_func, mais la variable locale, qui est déclarée dans le do ... tandis que la boucle de la macro.

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

6
répondu Mike Meyer 2011-12-21 22:42:56
la source

Je ne pense pas qu'il 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)

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

3
répondu John Nilsson 2008-10-19 02:04:41
la source

j'ai trouvé cette astuce très utile est dans les situations où vous devez traiter séquentiellement une valeur particulière. À chaque niveau de traitement, si une erreur ou une condition invalide se produit, vous pouvez éviter le traitement supplémentaire et éclater tôt. par exemple

#define CALL_AND_RETURN(x)  if ( x() == false) break;
do {
     CALL_AND_RETURN(process_first);
     CALL_AND_RETURN(process_second);
     CALL_AND_RETURN(process_third);
     //(simply add other calls here)
} while (0);
1
répondu pankaj 2015-11-18 07:34:54
la source

explication

do {} while (0) et if (1) {} else assurez-vous que la macro est élargi à seulement 1 instruction. Autrement:

if (something)
  FOO(X); 

s'étendrait à:

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

et g(X) seraient exécutés en dehors de la déclaration de contrôle if . Ceci est évité en utilisant do {} while (0) et if (1) {} else .


meilleure alternative

avec une GNU expression de déclaration (ne fait pas partie de la norme C), vous avez une meilleure façon que do {} while (0) et if (1) {} else de 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 do {} while (0) n'est pas), comme dans:

return FOO("X");
1
répondu Cœur 2018-08-05 08:32:28
la source

Autres questions sur c++ c c++-faq c-preprocessor