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

13 minutos de lectura

¿Por que usar instrucciones do while y if else aparentemente sin sentido
jfm3

En muchas macros de C/C++, veo el código de la macro envuelto en lo que parece un mensaje 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 escribir esto sin él?

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

  • Para el ejemplo con el else, agregaría una expresión de void escribir al final… como ((vacío)0).

    – Phil1970

    8 de marzo de 2018 a las 17:59

  • Recordatorio de que el do while construct no es compatible con declaraciones de retorno, por lo que el if (1) { ... } else ((void)0) construct tiene usos más compatibles en Standard C. Y en GNU C, preferirá la construcción descrita en mi respuesta.

    – Corazón

    12 dic 2018 a las 11:50

¿Por que usar instrucciones do while y if else aparentemente sin sentido
jfm3

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

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

Ahora, si tuvieras que usar BAR(X); en un if ... else instrucción, donde los cuerpos de la instrucción if no estuvieran encerrados entre corchetes, se llevarí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();

lo cual es sintácticamente incorrecto, ya que el else ya no está asociado con el if. No ayuda envolver 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 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 debe 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 para hacer 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 tienes que usar do ... whilepodrías cocinar algo con if ... else también, aunque cuando if ... else se expande dentro de un if ... else conduce a un “colgando más“, lo que podría hacer que un problema de otra cosa pendiente 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 colgando 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 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 despida el preprocesador C, este es el tipo de cosas que les preocupan.

  • ¿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.

    –Steve Melnikoff

    2 abr 2009 a las 22:51

  • El ejemplo de la coma debería ser #define BAR(X) (f(X), g(X)) de lo contrario, la precedencia del operador puede estropear la semántica.

    – Estuardo

    19 de mayo de 2011 a las 12:58

  • @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 todos los if instrucciones, etc., en nuestro código use llaves, luego envolver macros como esta es una forma simple de evitar problemas.

    –Steve Melnikoff

    20 de noviembre de 2013 a las 17:16

  • Nota la if(1) {...} else void(0) forma es más segura que la do {...} while(0) para macros cuyos parámetros son 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 MYMACRO Se 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.

    – Chris Kline

    22 de junio de 2015 a las 16:23


  • @as void(0) fue un error tipográfico, quise decir (void)0. Y creo esto lo hace Resuelva el problema de “colgando más”: observe que no hay punto y coma después del (void)0. Una otra cosa pendiente en ese caso (p. ej. if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) desencadena un error de compilación. Aquí está un ejemplo en vivo que lo demuestra

    – Chris Kline

    27 de julio de 2015 a las 11:07


¿Por que usar instrucciones do while y if else aparentemente sin sentido
parcebal

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:

Ayudar a que la macro se comporte 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 buena razón es que, en algún momento, el autor de la macro quizás necesite reemplazar la macro con una función genuina (quizás en línea). Entonces la macro debería De Verdad comportarse como uno.

Así que deberíamos tener una macro que necesite un 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 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 es usar 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 punto y coma + efectos de 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 ámbito, encapsulando así el código de la macro, y necesita un punto y coma al final, por lo que se expande al código que lo necesita.

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

  • Tenga en cuenta que cambiar las macros a la función en línea altera algunas macros estándar predefinidas, por ejemplo, el siguiente código muestra un cambio en FUNCIÓN y LÍNEA: #incluye #define Fmacro() printf(“%s %d\n”, FUNCIÓN, LÍNEA) vacío en línea Finline() { printf(“%s %d\n”, FUNCIÓN, LÍNEA); } int principal() { Fmacro(); Finline(); devolver 0; } (los términos en negrita deben estar entre guiones bajos dobles: ¡formateador incorrecto!)

    – Gnubi

    23 de agosto de 2012 a las 10:52


  • 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; los x en la expansión debería ser 32. Un tema más complejo es cuál es la expansión de MY_MACRO(i+7). Y otra es la expansión de MY_MACRO(0x07 << 6). Hay muchas cosas que son buenas, pero hay algunas i sin puntos y t sin cruzar.

    –Jonathan Leffler

    26 de agosto de 2013 a las 4:17

  • @Gnubie: Supongo que todavía está aquí y aún no se ha dado cuenta de esto: puede escapar de los asteriscos y guiones bajos en los comentarios con barras invertidas, así que si escribe \_\_LINE\_\_ 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 era cierto en 2012; han realizado bastantes mejoras en el motor desde entonces.

    – Scott

    22 de agosto de 2017 a las 23:07


  • Aprecio que mi comentario tenga seis años de retraso, pero la mayoría de los compiladores de C en realidad no están en línea inline funciones (según lo permitido por la norma)

    – Andrés

    3 de enero de 2019 a las 6:01

@ jfm3 – Tienes una buena respuesta a la pregunta. También es posible que desee agregar que el idioma macro también evita el comportamiento no deseado 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 un error de compilación, pero tiene la consecuencia probablemente no deseada de que siempre se llamará a g().

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 la do ... while al if ... else construir.

el problema de la if ... else construir es que no fuerza que pongas 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 el printf nunca se ejecutará la declaración.

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

1647632052 886 ¿Por que usar instrucciones do while y if else aparentemente sin sentido
Coeur

Explicación

do {} while (0) y if (1) {} else son 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) sería ejecutado fuera del if declaración de control. Esto se evita cuando se utiliza do {} while (0) y if (1) {} else.


Mejor alternativa

Con un GNU expresión de declaración (no es 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 lo es), como en:

return FOO("X");

  • 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 condicional. el do-while around se usa para imponer un punto y coma en los lugares donde se usa la macro. por lo tanto, se impone que la macro se comporte de manera más similar. esto incluye el requisito del punto y coma final cuando se usa.

    – Alexander Stohr

    21 de diciembre de 2018 a las 14:38


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

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

o aún 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 del #define :

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

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

  • 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 condicional. el do-while around se usa para imponer un punto y coma en los lugares donde se usa la macro. por lo tanto, se impone que la macro se comporte de manera más similar. esto incluye el requisito del punto y coma final cuando se usa.

    – Alexander Stohr

    21 de diciembre de 2018 a las 14:38


1647632053 763 ¿Por que usar instrucciones do while y if else aparentemente sin sentido
isaac schwabacher

de Jens Gustedt Biblioteca de preprocesador P99 (¡sí, el hecho de que tal cosa exista también me dejó alucinado!) mejora el if(1) { ... } else construir de una manera pequeña pero significativa definiendo 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) construir, break y continue todavía funciona 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 “colgando más” aquí, ya que el else se une al más cercano ifque 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 C, consulte esa biblioteca.

  • Si bien es muy inteligente, esto hace que uno sea bombardeado con advertencias del compilador sobre posibles cosas pendientes.

    – Segmentado

    26/03/2015 a las 19:20

  • Usualmente 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ó/finalizó afuera, eso es simplemente mal estilo y oculta posibles puntos de salida.

    – mirabilios

    21 de diciembre de 2016 a las 11:51

  • También hay una biblioteca de preprocesadores en Boost. ¿Qué tiene de alucinante?

    – René

    1 de junio de 2017 a las 8:45

  • El riesgo con else ((void)0) es que alguien podría estar escribiendo YOUR_MACRO(), f(); y será sintácticamente válido, pero nunca llamar f(). Con do while es un error de sintaxis.

    – melpomene

    7 junio 2019 a las 20:42

  • @melpomene, ¿y qué hay de else do; while (0)?

    – Carl Lei

    15 de agosto de 2019 a las 2:29

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad