946 votes

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

Dans de nombreuses macros C/C++, je vois le code de la macro enveloppé dans ce qui semble être un élément 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 vois pas ce que le do while fait. Pourquoi ne pas l'écrire sans elle ?

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

3 votes

Pour l'exemple avec le else, j'ajouterais une expression de void type à la fin... comme ((void)0) .

3 votes

Rappelons que le do while n'est pas compatible avec les instructions de retour, de sorte que l'option if (1) { ... } else ((void)0) a des usages plus compatibles avec le C standard. Et dans le C GNU, vous préférerez la construction décrite dans ma réponse.

991voto

jfm3 Points 13666

Le site 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 aviez 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 où les corps de l'instruction if ne sont pas entourés de crochets, vous auriez une mauvaise surprise.

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

Le code ci-dessus serait développé en

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

ce qui est syntaxiquement incorrect, car le else n'est plus associé au if. Il n'est pas utile d'entourer les choses d'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 ce problème. La première consiste à utiliser une virgule pour séquencer les instructions dans la macro sans lui enlever sa capacité à agir comme une expression.

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

La version ci-dessus de bar BAR transforme le code ci-dessus en ce qui suit, 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 être placé 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 faire en sorte que la macro soit une déclaration 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 d'utiliser do ... while vous pourriez cuisiner quelque chose avec if ... else également, bien que lorsque if ... else s'étend à l'intérieur d'un if ... else cela conduit à un " autre pendaison ", ce qui pourrait rendre un problème de dangling else existant encore plus difficile à trouver, comme dans le code suivant.

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

Il s'agit d'utiliser le point-virgule dans des contextes où un point-virgule qui pend est erroné. Bien sûr, on pourrait (et on devrait) argumenter à ce point qu'il serait mieux de déclarer BAR comme une fonction réelle, et non une macro.

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

0 votes

Dans ma propre réponse, j'ai oublié le problème "else si accolades et point-virgule", mais vous avez oublié le problème de la portée, ainsi que l'optimisation du compilateur de do/while(false)... :-p ...

2 votes

Mais pourquoi ne pas simplement #define FOO(X) { f(X) ; g(X) ; } ? ?

1 votes

@korona : if(expression) FOO(X) ; else ... ce genre de code serait invalide car vous avez 2 déclarations dans la clause if.

186voto

paercebal Points 38526

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

Il existe trois bonnes "astuces" pour y parvenir :

Aidez la macro à se comporter comme un véritable code

Le code normal est généralement terminé par un point-virgule. Si l'utilisateur voit un 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 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). 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 le montre la réponse de jfm3, il arrive que la macro contienne plus d'une instruction. Et si la macro est utilisée à l'intérieur d'une instruction if, cela posera problème :

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

Le site 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 est d'utiliser un scope :

#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 ?

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 nécessite un point-virgule à la fin, s'étendant ainsi au 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 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.
}

7 votes

Notez que le changement des macros en fonction inline modifie certaines macros prédéfinies standard, par exemple le code suivant montre un changement dans FONCTION et LIGNE : #include <stdio.h> #define Fmacro() printf("%s %d \n ", FONCTION , LIGNE ) inline void Finline() { printf("%s %d \n ", FONCTION , LIGNE ) ; } int main() { Fmacro() ; Finline() ; return 0 ; } (les termes en gras devraient être entourés de doubles traits de soulignement - mauvais formateur !)

8 votes

Cette réponse soulève un certain nombre de problèmes mineurs, mais pas totalement sans conséquence. Par exemple : void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; } n'est pas l'expansion correcte ; le x dans l'expansion devrait être de 32. Une question plus complexe est de savoir quelle est 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 points sur les i et des points sur les t.

0 votes

@Gnubie : Au cas où vous seriez encore là et que vous n'auriez pas encore compris : vous pouvez échapper aux astérisques et aux caractères de soulignement dans les commentaires avec des backslashes, donc si vous tapez \_\_LINE\_\_ Il est préférable d'utiliser le formatage de code pour le code, par exemple, __LINE__ (ce qui ne nécessite aucune manipulation particulière). P.S.Je ne sais pas si c'était vrai en 2012 ; ils ont fait pas mal d'améliorations au moteur depuis.

57voto

Michael Burr Points 181287

@jfm3 - Vous avez une bonne réponse à la question. Vous pourriez également ajouter que l'idiome de la macro empêche également le comportement involontaire, peut-être plus dangereux (parce qu'il n'y a pas d'erreur), des simples instructions "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 la conséquence probablement involontaire que g() sera toujours appelé.

24 votes

"probablement involontaire" ? J'aurais dit "certainement involontaire", ou alors il faut sortir le programmeur et lui tirer dessus (plutôt que de le châtier avec un fouet).

5 votes

Ou bien ils pourraient recevoir une augmentation, s'ils travaillent pour une agence à trois lettres et insèrent subrepticement ce code dans un programme open source largement utilisé... :-)

2 votes

Ce commentaire me rappelle la ligne d'échec dans le récent bogue de vérification des certificats SSL découvert dans les systèmes d'exploitation Apple.

25voto

ybungalobill Points 31467

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

Le problème de la if ... else est qu'elle ne force vous devez mettre le point-virgule. Comme dans ce code :

FOO(1)
printf("abc");

Bien que nous ayons omis le point-virgule (par erreur), le code se développera en

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

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

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

0 votes

C'est pourquoi vous devriez faire #define FOO(X) if (1) { f(X); g(X); } else (void)0

3 votes

@RichardHansen : Ce n'est toujours pas aussi bien, car en regardant l'invocation de la macro, on ne sait pas si elle s'étend à une déclaration ou à une expression. Si quelqu'un suppose cette dernière, il peut écrire FOO(1),x++; ce qui nous donnera à nouveau un faux positif. Il suffit d'utiliser do ... while et c'est tout.

1 votes

Il devrait suffire de documenter la macro pour éviter tout malentendu. Je suis d'accord pour dire que do ... while (0) est préférable, mais elle a un inconvénient : A break ou continue contrôlera le do ... while (0) et non la boucle contenant l'invocation de la macro. Ainsi, la if L'astuce a encore de la valeur.

17voto

Marius Points 2008

Bien que l'on s'attende à ce que les compilateurs optimisent l'élimination de la do { ... } while(false); boucles, il existe une autre solution qui ne nécessite pas cette construction. Cette solution consiste à 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 dans les cas où les variables sont construites et utilisées comme partie intégrante de l'instruction. #define :

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

Dans ce cas, on serait obligé d'utiliser la construction do/while.

0 votes

Merci, puisque l'opérateur virgule ne garantit pas l'ordre d'exécution, cette imbrication est un moyen de le faire respecter.

17 votes

@Marius : Faux ; l'opérateur virgule est un point de séquence et par conséquent fait ordre d'exécution de la garantie. Je pense que vous l'avez confondu avec la virgule dans les listes d'arguments de fonctions.

3 votes

La deuxième suggestion exotique a fait ma journée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X