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

StackOverflow https://stackoverflow.com/questions/154136

  •  03-07-2019
  •  | 
  •  

Question

Dans de nombreuses macros C / C ++, le code de la macro est encapsulé dans ce qui semble être une boucle sans signification et une 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 vois pas ce que le fait pendant que fait. Pourquoi ne pas simplement écrire ceci sans cela?

#define FOO(X) f(X); g(X)
Était-ce utile?

La solution

Le do ... while et si ... sinon sont là pour faire en sorte qu'un Le point-virgule après votre macro signifie toujours la même chose. Disons vous avait quelque chose comme votre deuxième macro.

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

Maintenant, si vous utilisiez BAR (X); dans une instruction if ... else , où les corps de l'instruction if n'étaient pas entourés d'accolades , vous auriez une mauvaise surprise.

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

Le code ci-dessus serait étendu à

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

est incorrect du point de vue de la syntaxe car le paramètre else n’est plus associé au if. Il n’est pas utile de mettre les éléments 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 manières de résoudre le problème. La première consiste à utiliser une virgule pour séquencer les instructions dans la macro sans lui priver de sa capacité à se comporter 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 complexe qui doit être inséré 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 prenant 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 d'utiliser do ... tandis que , vous pouvez également créer quelque chose avec si ... sinon , bien que lorsque si ... else se développe à l'intérieur d'un si ... else , il en résulte un " suspendu autrement " ;, ce qui pourrait rendre encore plus difficile la recherche d'un problème existant suspendu ailleurs, comme dans le code suivant.

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

Le but est d’utiliser le point-virgule dans des contextes où un point-virgule est erroné. Bien sûr, on pourrait (et devrait probablement) dire à ce stade qu’il serait préférable de déclarer BAR en tant que fonction réelle et non en tant que macro.

En résumé, le do ... while permet de contourner les défauts du préprocesseur C. Lorsque ces guides de style C vous disent de mettre à pied le préprocesseur C, vous vous inquiétez de ce genre de chose.

Autres conseils

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

Il existe trois bons "conseils". pour réussir en cela:

Aidez la macro à se comporter comme un code authentique

Le code normal se termine généralement par un point-virgule. Si le code utilisateur ne nécessite pas d’affichage ...

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

Cela signifie que 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 devra peut-être remplacer la macro par une fonction réelle (peut-être en ligne). La macro doit donc vraiment se comporter comme tel.

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

Produire un code valide

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

if(bIsOk)
   MY_MACRO(42) ;

Cette macro pourrait être développée comme suit:

#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 étendue à 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 ressemble à 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'agirait de:

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 à nouveau correctement.

Combinaison des effets point-virgule + portée?

Un idiome C / C ++ 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 nécessitant un point-virgule à la fin, développant ainsi le code qui en nécessite un.

Le bonus?

Le compilateur C ++ optimisera la boucle do / while, car le fait que sa post-condition soit fausse est connue 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 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é en tant que

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

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

   // Etc.
}

@ jfm3 - Vous avez une bonne réponse à la question. Vous voudrez peut-être aussi ajouter que l'idiome de la macro empêche également le comportement involontaire potentiellement 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 en:

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

ce qui est correct du point de vue de la syntaxe, donc il n’ya pas d’erreur de compilation, mais a probablement pour conséquence inattendue que g () sera toujours appelé.

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 construction if ... else .

Le problème de la construction if ... else est qu'il ne pas forcer à mettre le point-virgule. Comme dans ce code:

FOO(1)
printf("abc");

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

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

et va compiler en silence (bien que certains compilateurs puissent émettre un avertissement concernant le code inaccessible). Mais l'instruction printf ne sera jamais exécutée.

do ... while ne pose pas ce problème, car le seul jeton valide après le while (0) est un point-virgule.

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

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

ou même plus exotique:

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

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

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

Avec celui-ci, on serait obligé d'utiliser la construction do / while.

La bibliothèque de préprocesseur P99 de Jens Gustedt (oui, son existence mind too!) améliore la construction de if (1) {...} else 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 ne {...} tant que la construction (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 l'appel de macro, ce qui sinon ignorerait le bloc suivant. (Il n'y a pas réellement de problème "problème suspendu" dans la mesure où le else est lié au if le plus proche, qui est celui de la macro.)

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

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 dit que vous ne pouvez pas utiliser un 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 macros, vous utilisez le nom varié 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 voulue i, déclarée au début de some_func, mais la variable locale, déclarée dans la boucle do ... while de la macro.

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

Explication

do {} tandis que (0) et if (1) {} sinon permettent de s'assurer que la macro est étendue à 1 seule instruction. Sinon:

if (something)
  FOO(X); 

serait étendu à:

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

Et g (X) serait exécuté en dehors de l'instruction de contrôle si . Ceci est évité lorsque est utilisé {} avec (0) et si (1) {} sinon .

Meilleure alternative

Avec un GNU expression (ne fait pas partie de la norme C), vous avez une meilleure solution que faire {} tandis que (0) et si (1) {} autre à résoudre ceci, en utilisant simplement ({}) :

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

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

return FOO("X");

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 entraîner des erreurs intéressantes.

J'ai trouvé cette astuce très utile lorsque vous devez traiter séquentiellement une valeur particulière. À chaque niveau de traitement, si une erreur ou une condition non valide se produit, vous pouvez éviter un traitement supplémentaire et commencer plus 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);
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top