web-dev-qa-db-fra.com

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

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

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

Le do ... while et le if ... else permettent de faire en sorte qu'un point-virgule .__ après votre macro ait toujours le même sens. Disons que vous aviez 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();

ce qui est syntaxiquement incorrect, puisque le reste n’est plus associé au if. Cela n'aide pas d'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é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 bar 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'êtes pas obligé d'utiliser do ... while, vous pouvez aussi créer quelque chose avec if ... else, bien que lorsque if ... else se développe à l'intérieur d'un if ... else, il entraîne un " suspendu à l'autre ", ce qui pourrait rendre encore plus difficile le problème existant pendant au repos à trouver, 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 comme fonction réelle et non comme une macro.

En résumé, le do ... while permet de contourner les défauts du préprocesseur C. Quand ces guides de style C vous disent de mettre à pied le pré-processeur C, c'est le genre de chose qui les préoccupe.

752
jfm3

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 dans ce domaine:

Aidez la macro à se comporter comme un code authentique

Le code normal est généralement terminé par un point-virgule. Si l'utilisateur n'affiche pas le code nécessaire ...

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). Donc, la macro devrait 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 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 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 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'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.

Combinant les effets point-virgule + portée?

Il y a un langage C/C++ qui produit cet effet: la boucle do/while:

do
{
    // code
}
while(false) ;

Le do/while peut créer une étendue, 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 sa post-condition est 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.
}

va s'étendre 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é loin

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

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

   // Etc.
}
140
paercebal

@ jfm3 - Vous avez une bonne réponse à la question. Vous voudrez peut-être également 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);

s'étend à:

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

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

50
Michael Burr

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 n’est pas forcé de 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 compilera silencieusement (bien que certains compilateurs puissent émettre un avertissement pour le code inaccessible). Mais l'instruction printf ne sera jamais exécutée.

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

23
ybungalobill

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 avec les cas où des 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, on serait obligé d'utiliser la construction do/while.

15
Marius

La bibliothèque de préprocesseurs P99 de Jens Gustedt (oui, le fait qu'une telle chose existe m'a également éveillé l'esprit!) Améliore la construction 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 à la construction do { ... } while(0), break et continue fonctionnent toujours dans le 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 aurait pour effet d'ignorer le bloc suivant. (Il n'y a pas réellement de problème de "balançoire d'autre" ici, puisque la variable else se lie à la variable if la plus proche, qui est celle de la macro.) 

Si vous êtes intéressé par le type de tâches pouvant être effectuées de manière plus ou moins sûre avec le préprocesseur C, consultez cette bibliothèque.

10
Isaac Schwabacher

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 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!

6
Mike Meyer

Je ne pense pas que cela 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
John Nilsson

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);
2
pankaj

Explication

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

if (something)
  FOO(X); 

s'élargirait à:

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

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


Meilleure alternative

Avec une GNU statement expression (ne faisant pas partie de la norme C), vous avez un meilleur moyen de résoudre ce problème que do {} while (0) et if (1) {} else 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");
2
Cœur

La raison pour laquelle do {} while (0) est utilisé par rapport à if (1) {} est qu'il est impossible que quelqu'un modifie le code avant d'appeler la macro do {} while (0) pour qu'il s'agisse d'un autre type de bloc. Par exemple, si vous avez appelé une macro entourée de if (1) {} comme:

else
  MACRO(x);

C'est en fait un else if. Différence subtile

0
ericcurtin