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

788

En muchas macros C / C ++, veo el código de la macro envuelto en lo que parece un do whilebucle 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 do whileestá haciendo. ¿Por qué no escribir esto sin él?

#define FOO(X) f(X); g(X)
jfm3
fuente
2
Para el ejemplo con el else, agregaría una expresión de voidtipo al final ... like ((void) 0) .
Phil1970
1
Recuerde que la do whileconstrucción no es compatible con las declaraciones de retorno, por lo que la if (1) { ... } else ((void)0)construcción tiene usos más compatibles en el Estándar C. Y en GNU C, preferirá la construcción descrita en mi respuesta.
Cœur

Respuestas:

829

Los do ... whiley if ... elseestán ahí para que el punto y coma después de la macro siempre signifique lo mismo. Digamos que tenías algo así como tu segunda macro.

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

Ahora, si fuera a usar BAR(X);en una if ... elsedeclaración, donde los cuerpos de la declaración if no estuvieran envueltos entre llaves, obtendría una mala sorpresa.

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

El código anterior se expandiría en

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

que es sintácticamente incorrecto, ya que lo demás ya no está asociado con el if. No ayuda envolver cosas en 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 despojarla de su capacidad para actuar como una expresión.

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

La versión anterior de la barra BARexpande 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)tener un cuerpo de código más complicado que necesita 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 ... whilehacer que la macro sea una declaración única 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 tiene que usarlo do ... while, también podría cocinar algo if ... else, aunque cuando se if ... elseexpande dentro de un if ... elseconduce a un " colgar otro ", lo que podría hacer que un problema de colgar existente sea aún 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 colgante es erróneo. Por supuesto, podría (y probablemente debería) argumentarse en este punto que sería mejor declararlo BARcomo una función real, no como una macro.

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

jfm3
fuente
18
¿No es este un argumento fuerte para usar siempre llaves en las declaraciones if, while y for? Si siempre quiere hacer esto (como se requiere para MISRA-C, por ejemplo), el problema descrito anteriormente desaparece.
Steve Melnikoff el
17
El ejemplo de la coma debería ser, de lo #define BAR(X) (f(X), g(X))contrario, la precedencia del operador puede estropear la semántica.
Stewart
20
@DawidFerenczy: aunque tanto usted como yo, desde hace cuatro años y medio, tenemos un buen punto, tenemos que vivir en el mundo real. A menos que podamos garantizar que todas las ifdeclaraciones, etc., en nuestro código usen llaves, entonces envolver macros como esta es una manera simple de evitar problemas.
Steve Melnikoff
8
Nota: el if(1) {...} else void(0)formulario es más seguro que do {...} while(0)para las macros cuyos parámetros son código que se incluye en la expansión de macro, ya que no altera el comportamiento de la ruptura o continúa con las palabras clave. Por ejemplo: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }provoca un comportamiento inesperado cuando MYMACROse define como #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)porque la interrupción afecta el ciclo while de la macro en lugar del ciclo for en el sitio de llamada de macro.
Chris Kline
55
@ace void(0)era un error tipográfico, quise decir (void)0. Y creo que esto no resuelve el "colgando otro" problema: aviso no hay punto y coma después de la (void)0. Un otro colgante en ese caso (por ejemplo if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) desencadena un error de compilación. Aquí hay un ejemplo en vivo que lo demuestra
Chris Kline
153

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 un 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 buena razón es que, en algún momento, el autor de la macro tal vez necesite reemplazar la macro con una función genuina (tal vez en línea). Entonces la macro realmente debería comportarse como tal.

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

La gfunción se ejecutará independientemente del valor de bIsOk.

Esto significa que debemos agregar un ámbito 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 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, nuevamente, 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 nuevamente.

¿Combina los efectos de punto y coma + alcance?

Hay un modismo C / C ++ que produce este efecto: el bucle 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í en el código que lo necesita.

El bono?

El compilador de C ++ optimizará el ciclo 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 optimiza como

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

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

   // Etc.
}
paercebal
fuente
66
Tenga en cuenta que cambiar las macros a la función en línea altera algunas macros predefinidas estándar, por ejemplo, el siguiente código muestra un cambio en FUNCTION y LINE : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", FUNCTION , LINE ); } int main () {Fmacro (); Finline (); devuelve 0; } (los términos en negrita deben estar entre
comillas
66
Hay una serie de problemas menores pero no completamente intrascendentes con esta respuesta. Por ejemplo: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }no es la expansión correcta; el xde la expansión debería ser 32. Una cuestión más compleja es lo que es la expansión de MY_MACRO(i+7). Y otra es la expansión de MY_MACRO(0x07 << 6). Hay muchas cosas buenas, pero hay algunas i sin marcar y t sin cruzar.
Jonathan Leffler
@Gnubie: Creo que todavía estás aquí y aún no lo has descubierto: puedes escapar de los asteriscos y guiones bajos en los comentarios con barras diagonales inversas, así que si \_\_LINE\_\_lo escribes se representa como __LINE__. En mi humilde opinión, es mejor usar el formato de código para el código; por ejemplo, __LINE__(que no requiere ningún manejo especial). PD: No sé si esto fue cierto en 2012; Han hecho bastantes mejoras en el motor desde entonces.
Scott
1
Apreciando que mi comentario lleva seis años de retraso, pero la mayoría de los compiladores de C en realidad no tienen inlinefunciones en línea (como lo permite el estándar)
Andrew
53

@ jfm3: tiene una buena respuesta a la pregunta. Es posible que también desee agregar que el lenguaje idiomático también previene 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 para que no haya un error del compilador, pero tiene la consecuencia probablemente no intencionada de que siempre se llamará a g ().

Michael Burr
fuente
22
"probablemente no intencionado"? Hubiera dicho "ciertamente no intencionado", o de lo contrario el programador debe ser sacado y fusilado (en lugar de ser castigado rotundamente con un látigo).
Lawrence Dol
44
O podrían recibir un aumento, si están trabajando para una agencia de tres letras e insertando subrepticiamente ese código en un programa de código abierto ampliamente utilizado ... :-)
R .. GitHub DEJEN DE AYUDAR AL HIELO
2
Y este comentario solo me recuerda la línea de falla de goto en el reciente error de verificación de certificado SSL encontrado en los sistemas operativos de Apple
Gerard Sexton
23

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

El problema de la if ... elseconstrucción 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á en silencio (aunque algunos compiladores pueden emitir una advertencia para el código inalcanzable). Pero la printfdeclaración nunca se ejecutará.

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

Yakov Galka
fuente
2
@RichardHansen: Todavía no es tan bueno, porque al mirar la invocación de macro no sabes si se expande a una declaración o una expresión. Si alguien asume lo posterior, puede escribir FOO(1),x++;lo que nuevamente nos dará un falso positivo. Solo use do ... whiley eso es todo.
Yakov Galka
1
Documentar la macro para evitar el malentendido debería ser suficiente. Estoy de acuerdo en que do ... while (0)es preferible, pero tiene una desventaja: A breako continuecontrolará el do ... while (0)ciclo, no el ciclo que contiene la invocación de macro. Entonces el iftruco todavía tiene valor.
Richard Hansen
2
No veo dónde podría poner una breako una continueque se vería dentro del do {...} while(0)pseudo bucle de macros . Incluso en el parámetro macro haría un error de sintaxis.
Patrick Schlüter
55
Otra razón para usar en do { ... } while(0)lugar de if whateverconstruir, es la naturaleza idiomática de la misma. La do {...} while(0)construcción es generalizada, bien conocida y utilizada por muchos programadores. Su justificación y documentación se conocen fácilmente. No es así para la ifconstrucción. Por lo tanto, se requiere menos esfuerzo para asimilar cuando se hace una revisión de código.
Patrick Schlüter
1
@tristopia: He visto a personas escribir macros que toman bloques de código como argumentos (lo cual no necesariamente recomiendo). Por ejemplo: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Podría usarse como CHECK(system("foo"), break;);, donde break;está destinado a referirse al bucle que encierra la CHECK()invocación.
Richard Hansen
16

Si bien se espera que los compiladores optimicen los do { ... } while(false);bucles, 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 casos en los que las variables se construyan y utilicen como parte de #define:

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

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

Mario
fuente
gracias, dado que el operador de coma no garantiza el orden de ejecución, este anidamiento es una forma de aplicarlo.
Marius
15
@Marius: falso; el operador de coma es un punto de secuencia y, por lo tanto , garantiza el orden de ejecución. Sospecho que lo confundiste con la coma en las listas de argumentos de función.
R .. GitHub DEJA DE AYUDAR A HIELO
2
La segunda sugerencia exótica me alegró el día.
Spidey
Solo quería agregar que los compiladores se ven obligados a preservar el comportamiento observable del programa, por lo que la optimización de hacer / mientras no es un gran problema (suponiendo que las optimizaciones del compilador sean correctas).
Marco A.
@MarcoA. Si bien tiene razón, he encontrado en el pasado que la optimización del compilador, al tiempo que preserva exactamente la función del código, pero al cambiar las líneas que parecen no hacer nada en el contexto singular, romperá los algoritmos multiproceso. Caso en punto Peterson's Algorithm.
Marius
11

La biblioteca de preprocesador P99 de Jens Gustedt (sí, ¡el hecho de que tal cosa exista también me dejó boquiabierto !) Mejora la if(1) { ... } elseconstrucción 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 de la do { ... } while(0)construcción, breaky continuetodavía trabajan en el interior 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 otro modo saltarse el siguiente bloque. (No hay realmente un problema de "colgar más" aquí, ya que se elseune al más cercano if, que es el de la macro).

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

Isaac Schwabacher
fuente
Si bien es muy inteligente, esto hace que uno sea bombardeado con advertencias del compilador sobre posibles colgantes.
Segmentado el
1
Por lo general, usa macros para crear un entorno contenido, es decir, nunca usa un break(o continue) dentro de una macro para controlar un ciclo que comenzó / terminó afuera, eso es simplemente un mal estilo y oculta posibles puntos de salida.
mirabilos
También hay una biblioteca de preprocesador en Boost. ¿Qué es alucinante al respecto?
Rene
El riesgo else ((void)0)es que alguien podría estar escribiendo YOUR_MACRO(), f();y será sintácticamente válido, pero nunca llame f(). Con do whilees un error de sintaxis.
melpomene
@melpomene ¿y qué else do; while (0)?
Carl Lei
8

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

¡Algunos de ustedes mostraron macros con variables locales, pero nadie mencionó que no pueden usar cualquier nombre en una macro! ¡Morderá al usuario algún día! ¿Por qué? Porque los argumentos de entrada se sustituyen en su plantilla de macro. Y en sus ejemplos de macros que has utilizar el nombre variabled probablemente más utilizado 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 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 bucle do ... while de la macro.

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

Mike Meyer
fuente
El patrón habitual es agregar guiones bajos en nombres de variables en macros, por ejemplo int __i;.
Blaisorblade
8
@ Bladeorblade: En realidad, eso es incorrecto e ilegal C; los guiones bajos principales están reservados para su uso por la implementación. La razón por la que ha visto este "patrón habitual" es al leer los encabezados del sistema ("la implementación") que deben restringirse a este espacio de nombres reservado. Para aplicaciones / bibliotecas, debe elegir sus propios nombres oscuros, improbables de colisionar sin guiones bajos, por ejemplo, mylib_internal___io similares.
R .. GitHub DEJA DE AYUDAR AL HIELO
2
@R ... Tienes razón: en realidad he leído esto en una '' aplicación '', el kernel de Linux, pero de todos modos es una excepción, ya que no utiliza una biblioteca estándar (técnicamente, una implementación '' independiente '' C en su lugar de uno '' alojado '').
Blaisorblade
3
@R .. esto no es del todo correcto: los guiones bajos iniciales seguidos de un guión bajo o segundo subrayado están reservados para la implementación en todos los contextos. Los subrayados iniciales seguidos de otra cosa no están reservados en el ámbito local.
Leushenko
@Leushenko: Sí, pero la distinción es lo suficientemente sutil como para que sea mejor decirle a la gente que simplemente no use esos nombres. Las personas que entienden la sutileza presumiblemente ya saben que estoy pasando por alto los detalles. :-)
R .. GitHub DEJA DE AYUDAR A ICE
7

Explicación

do {} while (0)y if (1) {} elsedeben asegurarse de que la macro se expanda a solo 1 instrucción. De otra manera:

if (something)
  FOO(X); 

se expandiría a:

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

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


Mejor alternativa

Con una expresión de instrucción GNU (que no forma parte del estándar C), tiene una mejor manera que do {} while (0)y if (1) {} elsepara 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 lo es), como en:

return FOO("X");
Coeur
fuente
3
el uso de bloqueo de bloques {} en la macro sería suficiente para agrupar el código de macro para que todo se ejecute para la misma ruta de condición if. el do-while around se usa para forzar un punto y coma en los lugares donde se usa la macro. así, la macro se impone comportándose de manera más funcional por igual. esto incluye el requisito del punto y coma final cuando se usa.
Alexander Stohr
2

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++la macro la evalúa dos veces. Esto puede conducir a algunos errores interesantes.

John Nilsson
fuente
15
Esto no tiene nada que ver con la construcción do ... while (0).
Trent
2
Cierto. Pero pertinente al tema de macros vs. funciones y cómo escribir una macro que se comporte como una función ...
John Nilsson
2
De manera similar a la anterior, esta no es una respuesta sino un comentario. En el tema: es por eso que usas cosas solo una vez:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos