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

854

En muchas macros de C / C ++, veo el código de la macro envuelto en lo que parece un do whilebucle sin sentido . A continuación se muestran algunos 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)
2
  • 3
    Para el ejemplo con el else, agregaría una expresión de voidtipo al final ... como ((void) 0) . 8 de marzo de 2018 a las 17:59
  • 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
    12/12/18 a las 11:50
888

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

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

Ahora bien, si lo usara BAR(X);en una if ... elsedeclaración, donde los cuerpos de la declaración if no estuvieran entre paréntesis, obtendría 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 el if. No ayuda poner las cosas entre 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. La primera es usar una coma para secuenciar declaraciones dentro de la macro sin privarla de su capacidad para actuar como una expresión.

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

La versión anterior de bar BARexpande 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)tener 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 ... whilepara 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 cocinar algo if ... else, aunque cuando se if ... elseexpande dentro de un if ... else, conduce a un " otro colgante ", lo que podría hacer que un problema de otro colgante 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 utilizar el punto y coma en contextos donde un punto y coma que cuelga es erróneo. Por supuesto, podría (y probablemente debería) argumentarse en este punto que sería mejor declarar 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 apague el preprocesador C, este es el tipo de cosas que les preocupan.

12
  • 24
    ¿No es este un argumento sólido para usar siempre llaves en las declaraciones if, while y for? Si siempre hace esto (como se requiere para MISRA-C, por ejemplo), el problema descrito anteriormente desaparece. 2 abr 09 a las 22:51
  • 19
    El ejemplo de la coma debería ser, de lo #define BAR(X) (f(X), g(X))contrario, la precedencia de los operadores puede estropear la semántica.
    Stewart
    19 de mayo de 2011 a las 12:58
  • 21
    @DawidFerenczy: aunque tanto tú como yo, de 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 forma simple de evitar problemas. 20/11/2013 a las 17:16
  • 12
    Nota: el if(1) {...} else void(0)formulario es más seguro que el do {...} while(0)de las macros cuyos parámetros son el código que se incluye en la expansión de la macro, porque no altera el comportamiento de las palabras clave break o continue. 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 al bucle while de la macro en lugar del bucle for en el sitio de llamada de la macro. 22/06/15 a las 16:23
  • 6
    @ace void(0)fue un error tipográfico, me refiero (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 else 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. 27/07/15 a las 11:07
170

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:

Ayude a la macro a comportarse como un código genuino

El código normal suele terminar 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 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 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 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 vuelve a comportarse correctamente.

¿Combinar efectos de punto y coma + alcance?

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

do
{
    // code
}
while(false) ;

El do / while puede crear un alcance, 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.

¿El bono?

El compilador de C ++ optimizará el ciclo do / while, ya que el hecho de que su condición posterior es 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.
}
4
  • 6
    Tenga en cuenta que cambiar macros a función en línea altera algunas macros estándar predefinidas, 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 (); return 0; } (los términos en negrita deben ir entre dos guiones bajos - ¡mal formateador!)
    Gnubie
    23/08/12 a las 10:52
  • 6
    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 xen la expansión debería ser 32. Un tema más complejo es qué es la expansión de MY_MACRO(i+7). Y otro es la expansión de MY_MACRO(0x07 << 6). Hay muchas cosas buenas, pero hay algunas i sin marcar y algunas t sin cruzar. 26 de agosto de 2013 a las 4:17
  • @Gnubie: en caso de que todavía esté aquí y no se haya dado cuenta de esto a estas alturas: puede escapar de los asteriscos y guiones bajos en los comentarios con barras invertidas, por lo que si escribe, \_\_LINE\_\_se muestra como __LINE__. En mi humilde opinión, es mejor usar 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 realizado bastantes mejoras en el motor desde entonces.
    Scott
    22 de agosto de 2017 a las 23:07
  • 1
    Aprecio que mi comentario tiene un retraso de seis años, pero la mayoría de los compiladores de C no realizan inlinefunciones en línea (como lo permite el estándar)
    Andrew
    3 de enero de 2019 a las 6:01
55

@ jfm3: tienes una buena respuesta a la pregunta. Es posible que también desee agregar que el lenguaje macro también previene el comportamiento involuntario posiblemente más peligroso (porque no hay error) con declaraciones simples '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 ningún error del compilador, pero tiene la consecuencia probablemente involuntaria de que siempre se llamará a g ().

3
  • 24
    "probablemente no intencionado"? Habría dicho "ciertamente no intencionado", o de lo contrario hay que sacar al programador y dispararle (en lugar de castigarlo rotundamente con un látigo). 22/04/10 a las 2:45
  • 5
    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 ... :-) 26/04/12 a las 3:09
  • 2
    Y este comentario solo me recuerda la línea goto fail en el reciente error de verificación del certificado SSL encontrado en los sistemas operativos Apple 11/03/2014 a las 14:31
25

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 do ... whileel if ... elseconstructo.

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 compilará silenciosamente (aunque algunos compiladores pueden emitir una advertencia por código inalcanzable). Pero la printfdeclaración nunca se ejecutará.

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

6
  • 3
    @RichardHansen: Todavía no es tan bueno, porque al mirar la invocación de la macro no sabes si se expande a una declaración o una expresión. Si alguien asume lo último, puede escribir FOO(1),x++;lo que nuevamente nos dará un falso positivo. Solo usa do ... whiley listo. 3 de agosto de 2012 a las 17:35
  • 1
    Debería bastar con documentar la macro para evitar malentendidos. 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 la macro. Entonces el iftruco todavía tiene valor. 3 de agosto de 2012 a las 21:10
  • 2
    No veo dónde podría poner un breako un continueque se vería dentro de su do {...} while(0)pseudo-bucle de macros . Incluso en el parámetro macro, se produciría un error de sintaxis. 21/12/12 a las 16:02
  • 5
    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 está muy extendida, es bien conocida y es muy 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 necesita menos esfuerzo para asimilar al revisar el código. 21/12/12 a las 16:05
  • 1
    @tristopia: He visto a personas escribir macros que toman bloques de código como argumentos (que no recomiendo necesariamente). Por ejemplo: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Podría usarse como CHECK(system("foo"), break;);, donde break;se pretende hacer referencia al bucle que encierra la CHECK()invocación. 18/07/2013 a las 17:18
17

Si bien se espera que los compiladores optimicen los 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ótica:

#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 este uno se vería obligado a usar la construcción do / while.

6
  • gracias, dado que el operador de coma no garantiza el orden de ejecución, este anidamiento es una forma de hacer cumplir eso.
    Marius
    5/06/11 a las 13:39
  • dieciséis
    @Marius: Falso; el operador de coma es un punto de secuencia y, por lo tanto , garantiza el orden de ejecución. Sospecho que lo confundió con la coma en las listas de argumentos de funciones. 12 abr. 12 a las 5:02
  • 3
    La segunda sugerencia exótica me alegró el día.
    Spidey
    20 de junio de 2013 a las 19:08
  • Solo quería agregar que los compiladores están obligados a preservar el comportamiento observable del programa, por lo que optimizar el do / while away no es un gran problema (asumiendo que las optimizaciones del compilador son correctas). 31/03/15 a las 9:42
  • @MarcoA. Si bien tiene razón, he descubierto en el pasado que la optimización del compilador, al tiempo que conserva exactamente la función del código, pero al cambiar líneas que parecen no hacer nada en el contexto singular, romperá los algoritmos multiproceso. Caso en cuestión Peterson's Algorithm.
    Marius
    25/07/2016 a las 12:29
dieciséis

Explicación

do {} while (0)y if (1) {} elseson para asegurarse de que la macro se expanda 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 ifdeclaración de control. Esto se evita al usar do {} while (0)y if (1) {} else.


Mejor alternativa

Con una expresión de declaració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");
1
  • 4
    el uso de bloqueo de bloque {} en la macro sería suficiente para agrupar el código de la macro de modo que todo se ejecute para la misma ruta de condición if. el do-while around se usa para aplicar un punto y coma en los lugares donde se usa la macro. por lo tanto, la macro se hace cumplir comportándose de manera más similar. esto incluye el requisito del punto y coma al final cuando se usa. 21/12/18 a las 14:38
12

La biblioteca del 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. (En realidad, no hay 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 el tipo de cosas que se pueden hacer de forma más o menos segura con el preprocesador de C, consulte esa biblioteca.

5
  • Si bien es muy inteligente, esto hace que uno sea bombardeado con advertencias del compilador sobre posibles otros elementos colgantes. 26/03/15 a las 19:20
  • 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 bucle que comenzó / terminó afuera, eso es simplemente un mal estilo y oculta posibles puntos de salida. 21/12/2016 a las 11:51
  • También hay una biblioteca de preprocesadores en Boost. ¿Qué tiene de alucinante?
    Rene
    1 de junio de 2017 a las 8:45
  • El riesgo else ((void)0)es que alguien pueda estar escribiendo YOUR_MACRO(), f();y será sintácticamente válido, pero nunca llame f(). Con do whilees un error de sintaxis. 7/06/19 a las 20:42
  • @melpomene ¿y qué else do; while (0)? 15 de agosto de 2019 a las 2:29
8

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 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, ha utilizado el nombre de variable i, probablemente más utilizado .

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 usará la variable deseada i, que se declara al principio 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!

6
  • El patrón habitual es agregar guiones bajos en los nombres de las variables en las macros, por ejemplo int __i;. 22/12/11 a las 1:03
  • 8
    @Blaisorblade: En realidad, eso es incorrecto e ilegal C; los guiones bajos iniciales están reservados para que los utilice la implementación. La razón por la que ha visto este "patrón habitual" es por leer los encabezados del sistema ("la implementación") que deben restringirse a este espacio de nombres reservado. Para las aplicaciones / bibliotecas, debe elegir sus propios nombres oscuros y con poca probabilidad de colisionar sin guiones bajos, por ejemplo, mylib_internal___io similares. 12 abr 12 a las 5:05
  • 2
    @R .. Tienes razón. De hecho, he leído esto en una '' aplicación '', el kernel de Linux, pero de todos modos es una excepción ya que no usa una biblioteca estándar (técnicamente, una implementación C '' independiente '' en su lugar de uno '' alojado ''). 12/04/12 a las 16:23
  • 3
    @R .. esto no es del todo correcto: los guiones bajos iniciales seguidos de una mayúscula o un segundo guión bajo están reservados para la implementación en todos los contextos. Los guiones bajos iniciales seguidos de algo más no están reservados en el ámbito local. 3 de mayo de 2014 a las 3:15
  • @Leushenko: Sí, pero la distinción es lo suficientemente sutil que creo que es mejor decirle a la gente que no use esos nombres en absoluto. Las personas que entienden la sutileza presumiblemente ya saben que estoy pasando por alto los detalles. :-) 3 de mayo de 2014 y 3:16
2

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

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

se traduciría en

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

observe cómo i++la macro la evalúa dos veces. Esto puede provocar algunos errores interesantes.

3
  • 17
    Esto no tiene nada que ver con la construcción do ... while (0).
    Trent
    18 de nov de 2008 a las 19:54
  • 2
    Verdadero. Pero relevante para el tema de macros frente a funciones y cómo escribir una macro que se comporte como una función ... 26 de nov. De 2008 a las 19:21
  • 3
    De manera similar a la anterior, esta no es una respuesta sino un comentario. Sobre 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) 21/12/2016 a las 11:49