¿Por qué las macros de preprocesador son malvadas y cuáles son las alternativas?

Siempre he pedido esto, pero nunca he recibido una respuesta realmente buena; Creo que casi cualquier progtwigdor incluso antes de escribir el primer “Hola mundo” había encontrado una frase como “macro nunca debería ser utilizada”, “macro son malvados” y así sucesivamente, mi pregunta es: ¿por qué? Con el nuevo C ++ 11, ¿hay una alternativa real después de tantos años?

La parte fácil es sobre macros como #pragma , que son específicos de la plataforma y del comstackdor, y la mayoría de las veces tienen fallas serias como #pragma once que es propenso a errores en al menos 2 situaciones importantes: el mismo nombre en diferentes rutas y con algunas configuraciones de red y sistemas de archivos.

Pero, en general, ¿qué pasa con las macros y las alternativas a su uso?

Las macros son como cualquier otra herramienta: un martillo utilizado en un asesinato no es malo porque es un martillo. Es malo en la forma en que la persona lo usa de esa manera. Si quieres clavar clavos, un martillo es una herramienta perfecta.

Hay algunos aspectos de las macros que los hacen “malos” (ampliaré cada uno más adelante y sugiero alternativas):

  1. No puedes depurar macros.
  2. La expansión de macro puede provocar extraños efectos secundarios.
  3. Las macros no tienen “espacio de nombre”, por lo que si tiene una macro que choca con un nombre utilizado en otro lugar, obtiene reemplazos de macro donde no lo quería, y esto generalmente genera extraños mensajes de error.
  4. Las macros pueden afectar cosas que no te das cuenta.

Así que vamos a expandirnos un poco aquí:

1) Las macros no se pueden depurar. Cuando tiene una macro que se traduce en un número o una cadena, el código fuente tendrá el nombre de la macro y muchos depuradores, no podrá “ver” a qué se traduce la macro. Entonces, realmente no sabes lo que está pasando.

Reemplazo : use enum o const T

Para las macros “funcionales”, dado que el depurador funciona en un nivel “por línea de origen donde usted está”, su macro actuará como una statement única, sin importar si es una afirmación o cien. Hace que sea difícil descubrir qué está pasando.

Reemplazo : use funciones: en línea si necesita ser “rápido” (pero tenga en cuenta que demasiado en línea no es algo bueno)

2) Las expansiones de macro pueden tener efectos secundarios extraños.

El famoso es #define SQUARE(x) ((x) * (x)) y el uso x2 = SQUARE(x++) . Eso lleva a x2 = (x++) * (x++); , que, incluso si fuera un código válido [1], seguramente no sería lo que el progtwigdor quería. Si fuera una función, estaría bien hacer x ++, y x solo boostía una vez.

Otro ejemplo es “if else” en macros, digamos que tenemos esto:

 #define safe_divide(res, x, y) if (y != 0) res = x/y; 

y entonces

 if (something) safe_divide(b, a, x); else printf("Something is not set..."); 

En realidad, se convierte en algo completamente equivocado …

Reemplazo : funciones reales.

3) Las macros no tienen espacio de nombres

Si tenemos una macro:

 #define begin() x = 0 

y tenemos un código en C ++ que usa begin:

 std::vector v; ... stuff is loaded into v ... for (std::vector::iterator it = myvector.begin() ; it != myvector.end(); ++it) std::cout < < ' ' << *it; 

Ahora, ¿qué messenger de error crees que obtienes, y dónde buscas un error [suponiendo que hayas olvidado por completo o que ni siquiera conoces el macro de inicio que se encuentra en algún archivo de encabezado que alguien más escribió? [y aún más divertido si incluiste esa macro antes del include - te ahogarías en extraños errores que no tienen ningún sentido cuando miras el código.

Reemplazo : Bueno, no hay tanto como un reemplazo como una "regla": solo use nombres en mayúsculas para macros, y nunca use todos los nombres en mayúsculas para otras cosas.

4) Las macros tienen efectos que no te das cuenta

Toma esta función:

 #define begin() x = 0 #define end() x = 17 ... a few thousand lines of stuff here ... void dostuff() { int x = 7; begin(); ... more code using x ... printf("x=%d\n", x); end(); } 

Ahora, sin mirar la macro, pensarías que begin es una función, que no debería afectar a x.

Este tipo de cosas, y he visto ejemplos mucho más complejos, ¡PUEDEN estropear tu día REALMENTE!

Reemplazo : O no usa una macro para establecer x, o pasa x como un argumento.

Hay momentos en los que usar macros definitivamente es beneficioso. Un ejemplo es ajustar una función con macros para pasar información de archivo / línea:

 #define malloc(x) my_debug_malloc(x, __FILE__, __LINE__) #define free(x) my_debug_free(x, __FILE__, __LINE__) 

Ahora podemos usar my_debug_malloc como el malloc normal en el código, pero tiene argumentos adicionales, así que cuando llega el final y escaneamos "qué elementos de la memoria no se han liberado", podemos imprimir dónde se hizo la asignación de manera que el progtwigdor puede rastrear la fuga.

[1] Es un comportamiento indefinido actualizar una variable más de una vez "en un punto de secuencia". Un punto de secuencia no es exactamente lo mismo que una statement, pero para la mayoría de los propósitos, eso es lo que deberíamos considerar. Así que al hacer x++ * x++ se actualizará x dos veces, lo cual no está definido y probablemente genere valores diferentes en sistemas diferentes, y un valor de resultado diferente en x también.

El dicho “macros son malvados” generalmente se refiere al uso de #define, no de #pragma.

Específicamente, la expresión se refiere a estos dos casos:

  • definir números mágicos como macros

  • usando macros para reemplazar expresiones

con el nuevo C ++ 11, ¿hay una alternativa real después de tantos años?

Sí, para los elementos de la lista anterior (los números mágicos se deben definir con const / constexpr y las expresiones se deben definir con las funciones [normal / en línea / plantilla / plantilla en línea]).

Estos son algunos de los problemas introducidos al definir números mágicos como macros y reemplazar expresiones con macros (en lugar de definir funciones para evaluar esas expresiones):

  • al definir macros para números mágicos, el comstackdor no conserva información de tipo para los valores definidos. Esto puede causar advertencias de comstackción (y errores) y confundir a las personas para depurar el código.

  • al definir macros en lugar de funciones, los progtwigdores que usan ese código esperan que funcionen como funciones y no lo hacen.

Considera este código:

 #define max(a, b) ( ((a) > (b)) ? (a) : (b) ) int a = 5; int b = 4; int c = max(++a, b); 

Usted esperaría que a y c fueran 6 después de la asignación a c (como lo haría, con el uso de std :: max en lugar de la macro). En cambio, el código realiza:

 int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7 

Además de esto, las macros no admiten espacios de nombres, lo que significa que la definición de macros en el código limitará el código del cliente en los nombres que pueden usar.

Esto significa que si define la macro anterior (para max), ya no podrá incluir #include en ninguno de los códigos siguientes, a menos que escriba explícitamente:

 #ifdef max #undef max #endif #include  

Tener macros en lugar de variables / funciones también significa que no puede tomar su dirección:

  • si una macro-como-constante se evalúa como un número mágico, no se puede pasar por una dirección

  • para una macro como función, no puede usarlo como un predicado o tomar la dirección de la función o tratarla como un funtor.

Editar: como ejemplo, la alternativa correcta al #define max arriba:

 template inline T max(const T& a, const T& b) { return a > b ? a : b; } 

Esto hace todo lo que hace la macro, con una limitación: si los tipos de los argumentos son diferentes, la versión de la plantilla te obliga a ser explícito (lo que en realidad lleva a un código más seguro y más explícito):

 int a = 0; double b = 1.; max(a, b); 

Si este máximo se define como una macro, el código se comstackrá (con una advertencia).

Si este máximo se define como una función de plantilla, el comstackdor señalará la ambigüedad y tendrá que decir max(a, b) o max(a, b) (y así declarar explícitamente su intención) )

Un problema común es este:

 #define DIV(a,b) a / b printf("25 / (3+2) = %d", DIV(25,3+2)); 

Imprimirá 10, no 5, porque el preprocesador lo expandirá de esta manera:

 printf("25 / (3+2) = %d", 25 / 3 + 2); 

Esta versión es más segura:

 #define DIV(a,b) (a) / (b) 

Las macros son valiosas especialmente para crear código genérico (los parámetros de macro pueden ser cualquier cosa), a veces con parámetros.

Más, este código se coloca (es decir, se inserta) en el punto de la macro se utiliza.

OTOH, se pueden lograr resultados similares con:

  • funciones sobrecargadas (diferentes tipos de parámetros)

  • plantillas, en C ++ (tipos de parámetros generics y valores)

  • funciones en línea (coloque el código donde se llaman, en lugar de saltar a una definición de punto único; sin embargo, esto es más bien una recomendación para el comstackdor).

editar: en cuanto a por qué las macro son malas:

1) no hay verificación de tipo de los argumentos (no tienen ningún tipo), por lo que se pueden usar fácilmente 2) a veces se expanden en código muy complejo, que puede ser difícil de identificar y comprender en el archivo preprocesado 3) es fácil cometer un error -prone código en macros, tales como:

 #define MULTIPLY(a,b) a*b 

y luego llamar

 MULTIPLY(2+3,4+5) 

que se expande

2 + 3 * 4 + 5 (y no en: (2 + 3) * (4 + 5)).

Para tener lo último, debes definir:

 #define MULTIPLY(a,b) ((a)*(b)) 

Creo que el problema es que las macros no están bien optimizadas por el comstackdor y son “feas” de leer y depurar.

A menudo, una buena alternativa son funciones genéricas y / o funciones en línea.

No creo que haya nada de malo en usar definiciones de preprocesador o macros como las llamas.

Son un concepto de (meta) lenguaje que se encuentra en c / c ++ y, como cualquier otra herramienta, te pueden hacer la vida más fácil si sabes lo que estás haciendo. El problema con las macros es que se procesan antes de su código c / c ++ y generan un nuevo código que puede ser defectuoso y causar errores de comstackción que son obvios. Por el lado bueno, pueden ayudarlo a mantener su código limpio y ahorrarle mucho tipeo si lo usa correctamente, por lo que se reduce a sus preferencias personales.

Las macros en C / C ++ pueden servir como una herramienta importante para el control de versiones. El mismo código se puede entregar a dos clientes con una configuración menor de Macros. Uso cosas como

 #define IBM_AS_CLIENT #ifdef IBM_AS_CLIENT #define SOME_VALUE1 X #define SOME_VALUE2 Y #else #define SOME_VALUE1 P #define SOME_VALUE2 Q #endif 

Este tipo de funcionalidad no es tan fácil sin macros. Las macros son en realidad una gran herramienta de gestión de configuración de software y no solo una forma de crear accesos directos para la reutilización del código. La definición de funciones para la reutilización en macros definitivamente puede crear problemas.