Pregunta

En muchas macros de C / C ++ estoy viendo el código de la macro envuelto en lo que parece ser un bucle do while sin sentido. 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 hace mientras está haciendo. ¿Por qué no escribir esto sin él?

#define FOO(X) f(X); g(X)
¿Fue útil?

Solución

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

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

Ahora, si usara BAR (X); en una declaración de if ... else , donde los cuerpos de la declaración if no estaban entre corchetes , obtendrí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 la otra cosa ya no está asociada con el if. No ayuda encerrar las cosas entre llaves en 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 las declaraciones dentro de la macro sin privarle de 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 a 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 debe ir en su propio bloque, por ejemplo, para declarar variables locales. En el caso más general, la solución es usar algo como do ... while para hacer que la macro sea una sola declaración que tome un punto y coma sin confusión.

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

No tiene que usar do ... while , también puede preparar algo con si ... else también, aunque cuando si ... else se expande dentro de un si ... else conduce a un " hang else else " ;, lo que podría hacer que un problema existente de else else sea más difícil de encontrar, 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 colgado es erróneo. Por supuesto, podría (y probablemente debería) argumentarse en este punto que sería mejor declarar BAR como una función real, no como una macro.

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

Otros consejos

Las macros son piezas de texto copiadas / pegadas 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 la macro se comporte como código genuino

El código normal generalmente termina con un punto y coma. Si el usuario ve 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 de la macro quizás deba reemplazar la macro con una función genuina (quizás en línea). Así que la macro debería realmente comportarse como tal.

Por lo tanto, deberíamos tener una macro que necesite un punto y coma.

Produce 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 utiliza dentro de una sentencia 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) ;

La función g 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) ;

Produce un código válido 2

Si la macro es algo 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 compilará, por supuesto. Así que, nuevamente, la solución está utilizando 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 los puntos de semón y los efectos de alcance?

Hay un lenguaje C / C ++ que produce este efecto: el bucle do / while:

do
{
    // code
}
while(false) ;

El do / while puede crear un alcance, por lo tanto, encapsula el código de la macro, y necesita un punto y coma al final, expandiéndose así al código que necesita uno.

¿El bono?

El compilador de C ++ optimizará el bucle do / while, ya que el hecho de que su condición posterior sea falsa se conoce en el momento de la 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 se optimiza como

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

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

   // Etc.
}

@ jfm3 - Tiene una buena respuesta a la pregunta. También es posible que desee agregar que el lenguaje macro también evita el comportamiento no deseado posiblemente más peligroso (porque no hay error) con simples declaraciones "si":

#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 intencionada de que g () siempre será llamado.

Las respuestas anteriores explican el significado de estas construcciones, pero hay una diferencia significativa entre las dos que no se mencionaron. De hecho, hay una razón para preferir el do ... while al if ... else .

El problema de la construcción si ... else es que no te obliga a poner el punto y coma. Como en este código:

FOO(1)
printf("abc");

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

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

y se compilará silenciosamente (aunque algunos compiladores pueden emitir una advertencia para el código inalcanzable). Pero la sentencia printf nunca se ejecutará.

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

Aunque se espera que los compiladores optimicen el do {...} while (falso); , existe 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 construyen y se usan como parte de #define :

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

Con este, se vería obligado a usar la construcción do / while.

biblioteca del preprocesador de P99 de Jens Gustedt (sí, el hecho de que exista tal cosa hizo explotar mi mind too!) mejora el if (1) {...} else de una manera pequeña pero significativa al definir 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) , break y continue aún funcionan dentro del bloque dado, pero el ((void) 0) crea un error de sintaxis si se omite el punto y coma después de la llamada a la macro, que de lo contrario omitiría el siguiente bloque. (En realidad, no hay un problema de " que cuelga else " aquí, ya que else se enlaza al if más cercano , que es el que está en la macro.)

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

Por algunas razones no puedo comentar sobre la primera respuesta ...

Algunos de ustedes mostraron macros con variables locales, ¡pero nadie mencionó que no puede usar ningún nombre en una macro! ¡Morderá al usuario algún día! ¿Por qué? Debido a que los argumentos de entrada se sustituyen en su plantilla de macro. Y en sus ejemplos de macros, ha usado el nombre variable más utilizado probablemente i .

Por ejemplo, cuando la siguiente macro

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

se utiliza en la siguiente función

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

la macro no utilizará la variable deseada i, que se declara al principio de some_func, pero la variable local, que se declara en el bucle do ... while de la macro.

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

Explicación

do {} while (0) y si (1) {} else son para asegurarse de que la macro se expande a solo 1 instrucción. De lo contrario:

if (something)
  FOO(X); 

se expandiría a:

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

Y g (X) se ejecutaría fuera de la declaración de control if . Esto se evita al usar do {} while (0) y if (1) {} else .


Mejor alternativa

Con una expresión de la declaración (no es una parte del estándar C), tiene una mejor manera que do {} while (0) y if (1) {} else para resolver esto, simplemente usando ({}) :

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

Y esta sintaxis es compatible con los valores de retorno (tenga en cuenta que do {} while (0) no es), como en:

return FOO("X");

No creo que se haya mencionado, así que considera esto

while(i<100)
  FOO(i++);

se traduciría a

while(i<100)
  do { f(i++); g(i++); } while (0)

observe cómo i ++ es evaluado dos veces por la macro. Esto puede llevar a algunos errores interesantes.

Este truco me ha resultado muy útil en situaciones en las que tienes que procesar secuencialmente un valor en particular. En cada nivel de procesamiento, si se produce algún error o condición no válida, puede evitar el procesamiento adicional y romper antes. por ejemplo

#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);
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top