Pregunta ¿Por qué usar sentencias do-while y if-else aparentemente sin sentido en macros?


En muchas macros de C / C ++ veo el código de la macro envuelto en lo que parece ser un sin sentido do while lazo. Aquí hay ejemplos.

#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else

No puedo ver lo que do while está haciendo. ¿Por qué no solo escribir esto sin eso?

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

650
2017-09-30 17:36


origen


Respuestas:


los do ... while y if ... else están ahí para hacerlo de modo que una punto y coma después de su macro siempre significa lo mismo. Digamos que tenía algo así como tu segunda macro.

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

Ahora si fuera a usar BAR(X); en una if ... else declaración, donde los cuerpos de la declaración if no estaban envueltos en llaves, te darías una mala sorpresa.

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

El código anterior se expandiría a

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

que es sintácticamente incorrecto, ya que el else ya no está asociado con if. No ayuda a envolver las cosas con llaves dentro de la macro, porque un punto y coma después de las llaves es sintácticamente incorrecto.

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

Hay dos formas de solucionar el problema. El primero es usar una coma para secuenciar declaraciones dentro de la macro sin quitarle su capacidad de actuar como una expresión.

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

La versión anterior de la barra BAR expande el código anterior en lo que sigue, que es sintácticamente correcto.

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

Esto no funciona si en lugar de f(X) tiene un cuerpo de código más complicado que necesita ir en su propio bloque, digamos, por ejemplo, para declarar variables locales. En el caso más general, la solución es usar algo como do ... while hacer que la macro sea una sola declaración que toma un punto y coma sin confusión.

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

No tienes que usar do ... while, podrías cocinar algo con if ... else también, aunque cuando if ... else se expande dentro de una if ... else conduce a un "colgando algo", lo que podría dificultar aún más la existencia de un problema existente colgando, como en el siguiente código.

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

El punto es usar el punto y coma en contextos donde un punto y coma es erróneo. Por supuesto, podría (y probablemente debería) ser discutido en este punto que sería mejor declarar BAR como una función real, no una macro.

En resumen, el do ... while está ahí para solucionar las deficiencias del preprocesador C. Cuando esas guías de estilo C le dicen que suspenda el preprocesador C, este es el tipo de cosas que les preocupan.


715
2017-09-30 17:36



Las macros son fragmentos de texto copiados / pegados que el preprocesador colocará en el código original; el autor de la macro espera que el reemplazo produzca un código válido.

Hay tres buenos "consejos" para tener éxito en eso:

Ayuda a que el macro se comporte como un código genuino

El código normal generalmente termina con un punto y coma. ¿Debería el usuario ver el código que no necesita uno ...

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

Esto significa que el usuario espera que el compilador produzca un error si el punto y coma está ausente.

Pero la verdadera razón real es que, en algún momento, el autor del macro tal vez necesite reemplazar la macro con una función genuina (tal vez en línea). Entonces, la macro debería De Verdad comportarse como uno.

Entonces deberíamos tener una macro que necesite punto y coma.

Producir un código válido

Como se muestra en la respuesta de jfm3, a veces la macro contiene más de una instrucción. Y si la macro se usa dentro de una declaración if, esto será problemático:

if(bIsOk)
   MY_MACRO(42) ;

Esta macro podría expandirse como:

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

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

los g la función se ejecutará independientemente del valor de bIsOk.

Esto significa que debemos tener que agregar un alcance a la macro:

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

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

Producir un código válido 2

Si la macro es algo así como:

#define MY_MACRO(x) int i = x + 1 ; f(i) ;

Podríamos tener otro problema en el siguiente código:

void doSomething()
{
    int i = 25 ;
    MY_MACRO(32) ;
}

Porque se expandiría como:

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

Este código no se compilará, por supuesto. Entonces, de nuevo, la solución está usando un alcance:

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

El código se comporta correctamente de nuevo.

¿Combinando efectos de alcance de dos puntos +?

Hay un modismo C / C ++ que produce este efecto: El ciclo do / while:

do
{
    // code
}
while(false) ;

El do / while puede crear un ámbito, encapsulando así el código de la macro, y necesita un punto y coma al final, expandiéndose así al código que necesita uno.

La bonificación?

El compilador de C ++ optimizará el bucle do / while, ya que el hecho de que su condición posterior sea falsa se conoce en tiempo de compilación. Esto significa que una macro como:

#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 expandirá correctamente como

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

y luego se compila y optimiza como

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

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

   // Etc.
}

131
2017-09-30 18:12



@ jfm3 - Tienes una buena respuesta a la pregunta. También es posible que desee agregar que la expresión idiomática macro también evita el comportamiento involuntario posiblemente más peligroso (porque no hay error) con simples declaraciones 'if':

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

if (test) FOO( baz);

se expande a:

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

que es sintácticamente correcto por lo que no hay error de compilación, pero tiene la consecuencia probablemente no intencional de que g () siempre se llamará.


50
2017-08-03 15:21



Las respuestas anteriores explican el significado de estos constructos, pero hay una diferencia significativa entre los dos que no se mencionó. De hecho, hay una razón para preferir el do ... while al if ... else construir.

El problema de if ... else construir es que no lo hace fuerza a poner el punto y coma Al igual que en este código:

FOO(1)
printf("abc");

Aunque dejamos fuera el punto y coma (por error), el código se expandirá a

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

y compilará silenciosamente (aunque algunos compiladores pueden emitir una advertencia de código inalcanzable). Pero el printf declaración nunca será ejecutada.

do ... while construct no tiene ese problema, ya que el único token válido después del while(0) es un punto y coma


21
2017-10-10 08:27



Si bien se espera que los compiladores optimicen la do { ... } while(false); bucles, hay otra solución que no requeriría esa construcción. La solución es usar el operador de coma:

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

o incluso más exóticamente:

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

Si bien esto funcionará bien con instrucciones separadas, no funcionará con los casos en que las variables se construyan y se usen como parte del #define :

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

Con esto, se vería forzado a usar el constructo do / while.


15
2017-11-19 16:19



Jens Gustedt Biblioteca de preprocesador P99 (¡sí, el hecho de que tal cosa existe también me voló la cabeza!) mejora el if(1) { ... } else construir de una manera pequeña pero significativa mediante la definición de lo siguiente:

#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP

La razón de esto es que, a diferencia del do { ... } while(0)construir, break y continue seguir trabajando dentro del bloque dado, pero el ((void)0) crea un error de sintaxis si el punto y coma se omite después de la llamada a la macro, que de lo contrario saltaría el siguiente bloque. (En realidad, no hay un problema de "otro que cuelga" aquí, ya que else se une al más cercano if, que es el que está en la macro.)

Si está interesado en el tipo de cosas que se pueden hacer de manera más o menos segura con el preprocesador C, revise esa biblioteca.


8
2017-12-21 18:42



Por alguna razón, no puedo comentar sobre la primera respuesta ...

Algunos de ustedes mostraron macros con variables locales, ¡pero nadie mencionó que no pueden usar ningún nombre en una macro! Morderá al usuario algún día! ¿Por qué? Porque los argumentos de entrada se sustituyen en su macro plantilla. Y en los ejemplos de tu macro, has usado el nombre variable más comúnmente usado yo.

Por ejemplo, cuando la siguiente macro

#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)

se usa en la siguiente función

void some_func(void) {
    int i;
    for (i = 0; i < 10; ++i)
        FOO(i);
}

la macro no usará la variable deseada i, que se declara al comienzo de some_func, sino la variable local, que se declara en el ciclo do ... while de la macro.

Por lo tanto, ¡nunca use nombres comunes de variables en una macro!


6
2017-10-18 22:04