Question Pourquoi utiliser des instructions do-while et if-else apparemment sans signification dans les macros?


Dans de nombreuses macros en C / C ++, je vois le code de la macro enveloppé dans ce qui semble être sans signification do while boucle. 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 peux pas voir ce que le do while fait. Pourquoi ne pas écrire cela sans ça?

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

650
2017-09-30 17:36


origine


Réponses:


le 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 toi avait quelque chose comme votre deuxième macro.

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

Maintenant, si vous deviez utiliser BAR(X); dans un if ... else déclaration, où les corps de la déclaration if ne sont pas enveloppés dans des accolades, vous obtiendrez 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 l'autre n'est plus associé à l'if. Cela n'aide pas à envelopper 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 y a deux façons de régler le problème. La première consiste à utiliser une virgule pour séquencer les instructions dans 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 dé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 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 ... while pour 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'êtes pas obligé d'utiliser do ... while, tu pourrais cuisiner quelque chose avec if ... else aussi, bien que quand if ... else se développe à l'intérieur d'un if ... else cela conduit à un "balançant d'autre», ce qui pourrait rendre un problème existant 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 flottant est erroné. Bien sûr, il pourrait (et devrait probablement) être argumenté à ce stade qu'il vaudrait mieux déclarer BAR en tant que fonction réelle, pas une macro.

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


715
2017-09-30 17:36



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

Il y a trois bons «conseils» pour y parvenir:

Aide la macro à se comporter comme du vrai code

Le code normal est généralement terminé 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) ;

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

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

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

Produire un code valide

Comme indiqué dans la réponse de jfm3, il arrive que la macro contienne plus d'une instruction. Et si la macro est utilisée dans une instruction if, cela 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) ;

le g la fonction sera exécutée indépendamment de 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 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 correctement à nouveau.

Combinaison de points-virgules et d'effets de portée?

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 en code en ayant 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.
}

va se développer correctement comme

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.
}

131
2017-09-30 18:12



@ jfm3 - Vous avez une bonne réponse à la question. Vous pouvez également ajouter que l'idiome de macro empêche également le comportement involontaire plus dangereux (car il n'y a pas d'erreur) avec des instructions simples 'if':

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

if (test) FOO( baz);

se développe pour:

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

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


50
2017-08-03 15:21



Les réponses ci-dessus expliquent la signification de ces constructions, 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 do ... while au if ... else construction.

Le problème de la if ... else construire est qu'il ne le fait pas Obliger 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 s'étendra jusqu'à

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

et compilera silencieusement (bien que certains compilateurs puissent émettre un avertissement pour le code inaccessible). Mais le printf L'instruction ne sera jamais exécutée.

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


21
2017-10-10 08:27



Alors que l'on s'attend à ce que les compilateurs optimisent do { ... } while(false); boucles, il existe une autre solution qui ne nécessiterait pas cette construction. La solution consiste à utiliser l'opérateur de virgule:

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

ou même plus exotiquement:

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

Bien que cela fonctionnera 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 obligé d'utiliser la construction do / while.


15
2017-11-19 16:19



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

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


8
2017-12-21 18:42



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 pouviez 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 de macro. Et dans vos exemples de macro, vous avez utilisé le nom variable probablement le plus utilisé je.

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 la boucle do ... while de la macro.

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


6
2017-10-18 22:04



Je ne pense pas qu'il a é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.


3
2018-03-23 12:04



Mieux que do {} while (0) et if (1) {} else, on peut simplement utiliser ({}):

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

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

return FOO("X");

1
2018-06-24 17:45