Macros con argumentos: paréntesis, evaluación múltiple y do while 0
Una macro de función no es una función: sustituye texto sin conocer la precedencia, los tipos ni los efectos secundarios. Esta lección deriva las tres disciplinas que hacen una macro segura —paréntesis en todos los niveles, un solo uso de cada parámetro y el envoltorio do while 0 para sentencias— y fija el criterio para preferir static inline siempre que se pueda.
Una macro con argumentos se parece tanto a una función que casi todo el mundo la escribe como si lo fuera, y ahí empiezan los problemas. El preprocesador no conoce la precedencia de operadores, no conoce los tipos y no sabe qué es un efecto secundario: solo sabe pegar el texto que le diste donde aparece el parámetro. Las tres reglas clásicas —paréntesis obligatorios, evaluación única y do ... while 0— no son manías de estilo, son la reconstrucción manual de todo lo que una función te daba gratis.
- Colocar los paréntesis en los dos niveles que hacen falta y saber por qué son dos.
- Reconocer la evaluación múltiple como defecto de corrección y de rendimiento.
- Aplicar
do ... while 0para que una macro de sentencias se comporte como una sentencia. - Decidir con criterio entre macro y
static inlineen C23.
La sustitución es textual, y por eso hacen falta paréntesis
Antes de nada, una trampa de sintaxis: la lista de parámetros debe ir pegada al nombre. Si escribes un espacio entre el identificador y el paréntesis de apertura, defines una macro de objeto cuyo cuerpo empieza por un paréntesis, no una macro de función. El compilador no protesta y el resultado es incomprensible.
Con la definición correcta, el peligro real es que el preprocesador pega texto crudo y el analizador sintáctico lo reagrupa después según la precedencia. Eso rompe la expresión por dos sitios distintos, y por eso los paréntesis van en dos niveles.
#define DOBLE(x) 2*x
DOBLE(1 + 2) /* -> 2*1 + 2 = 4, no 6: el argumento se parte */
#define DOBLE(x) 2*(x)
10/DOBLE(5) /* -> 10/2*(5) = 25, no 1: el resultado se absorbe */
#define DOBLE(x) (2*(x)) /* correcto en ambos contextos */
El paréntesis interno protege al argumento de los operadores del cuerpo de la macro; el paréntesis externo protege al cuerpo de los operadores que rodean la invocación. Omitir cualquiera de los dos deja un fallo latente que solo aparece cuando alguien usa la macro en un contexto que tú no probaste, normalmente meses después.
Hay una tercera fractura que los paréntesis no arreglan solos. El preprocesador separa los argumentos por comas de nivel superior, así que cualquier coma no encerrada en paréntesis parte el argumento en dos. Una llamada con un inicializador entre llaves o con un tipo que lleve coma falla con un mensaje de aridad que no dice nada útil; la defensa es envolver el argumento en el punto de uso o declarar la macro variádica y usar __VA_ARGS__.
Una función rechaza un argumento del tipo equivocado en la línea de la llamada. Una macro lo acepta siempre y el error aparece más adelante, dentro del texto expandido, señalando una línea que tú no escribiste. Por eso los diagnósticos de las macros son notoriamente malos: el compilador informa sobre el resultado de la sustitución, no sobre la fuente. Compilar con -E y leer la expansión es a menudo la vía más rápida para entender un error dentro de una macro.
Evaluación múltiple: el defecto que no se ve
Si un parámetro aparece dos veces en el cuerpo, el argumento se escribe dos veces, y con él todos sus efectos secundarios. Esto no es una ineficiencia: es comportamiento indefinido cuando el argumento modifica un objeto, y es una llamada duplicada cuando el argumento es una función.
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int i = 0, j = 5;
int m = MAX(i++, j); /* i se modifica dos veces: UB */
int n = MAX(consulta(), 0); /* consulta se ejecuta dos veces */
El segundo caso es el más insidioso porque el programa es correcto y solo va lento, o correcto salvo que consulta lleve un contador o lea de un socket. Ninguna advertencia estándar lo detecta, porque desde el punto de vista del compilador ahí hay dos llamadas legítimas.
C23 mejora la situación al estandarizar typeof y typeof_unqual, que permiten declarar variables temporales con el tipo del argumento sin conocerlo. Con la extensión de expresiones-sentencia de GCC y Clang, eso da una macro que evalúa una sola vez y no fija tipos; sin ella, el remedio portable es una función.
/* extension de GCC y Clang, no estandar: expresion-sentencia */
#define MAX_SEGURO(a, b) \
({ typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; })
/* portable y preferible cuando los tipos son conocidos */
static inline int max_i(int a, int b) { return a > b ? a : b; }
Fíjate en el detalle de higiene: los temporales llevan un prefijo poco probable. Si la macro declarase a y b, cualquier invocación cuyo argumento mencione una variable con ese nombre quedaría capturada por la declaración interna. El preprocesador no tiene ámbitos, así que la higiene léxica hay que fabricarla a mano con nombres feos.
Conviene añadir aquí una regla del estándar que rara vez se enseña y que explica comportamientos aparentemente mágicos. Mientras se expande una macro, su propio nombre queda marcado como no reemplazable dentro del resultado: si el cuerpo vuelve a mencionarla, esa aparición se deja tal cual y no se expande otra vez. La consecuencia es que las macros no pueden ser recursivas y que la construcción #define getc(s) getc_impl(s) no entra en un bucle infinito. La consecuencia práctica, más útil, es que puedes definir una macro con el mismo nombre que una función y usarla para envolverla, algo que la biblioteca estándar hace constantemente.
El patrón do while 0
El segundo grupo de macros no calcula un valor sino que ejecuta varias sentencias. Aquí el problema no es la precedencia sino la gramática: una macro de varias sentencias deja de ser una sentencia, y eso rompe todo contexto donde la gramática exige exactamente una.
#define REGISTRA(m) fputs(m, log); fflush(log);
if (fallo)
REGISTRA("error"); /* fflush queda FUERA del if: se ejecuta siempre */
else
sigue(); /* y ademas el else no compila */
Envolver el cuerpo en llaves resuelve el primer defecto y agrava el segundo: el bloque termina en llave, el punto y coma del punto de uso se convierte en una sentencia vacía y el else queda huérfano. La solución canónica es envolver el cuerpo en un bucle que se ejecuta una vez y nunca repite.
#define REGISTRA(m) \
do { \
fputs((m), log); \
fflush(log); \
} while (0)
La construcción funciona porque do ... while solo está completa con su punto y coma final: la macro es sintácticamente una sentencia única y exige el punto y coma en el punto de uso, igual que una llamada a función. Con optimizaciones, el bucle desaparece por completo del código generado.
flowchart TB A[macro con varias sentencias] --> B[cuerpo desnudo sin envoltorio] A --> C[cuerpo entre llaves] A --> D[patron do while cero] B --> E[solo la primera sentencia entra en el if] C --> F[el punto y coma del uso rompe el else] D --> G[una sola sentencia que exige punto y coma] E --> H[bug silencioso o error de compilacion] F --> H G --> I[se comporta como una llamada a funcion] style H fill:#f38ba8,color:#11111b style I fill:#a6e3a1,color:#11111b
Dos avisos sobre el idioma. Si la macro debe producir un valor, do ... while 0 no sirve: usa el operador coma o una expresión-sentencia. Y si tu compilador advierte de condición constante, la práctica habitual es silenciar esa advertencia concreta en la definición, no abandonar el patrón.
Merece la pena ver las tres reglas de esta lección como lo que realmente son: la reconstrucción artesanal, caso por caso, de propiedades que el lenguaje ya te regalaba en cuanto escribías static inline. Una función delimita sus argumentos como expresiones completas, y eso hace innecesarios los paréntesis. Una función evalúa cada argumento exactamente una vez, y eso elimina de raíz el problema de los efectos secundarios duplicados. Una función es una expresión primaria, así que ningún operador circundante puede reagruparla. Y una llamada a función ya es una sentencia cuando le pones el punto y coma, con lo que do ... while 0 sobra. Cada regla de macro que memorizas es la cicatriz de una garantía perdida. La consecuencia práctica en C23 es contundente: como static inline produce el mismo código máquina que la macro con -O2 —el compilador inserta el cuerpo y el nombre no llega ni al enlazador—, ya no queda ninguna razón de rendimiento para escribir macros de función. Lo único que justifica una macro es aquello que una función no puede hacer por definición: manipular el texto del programa con # y ##, capturar el contexto léxico del punto de uso con __FILE__ y __LINE__, aparecer donde se exige una expresión constante o despachar por tipo con _Generic. Si tu macro no hace ninguna de esas cuatro cosas, no es una macro: es una función mal escrita que perdió la comprobación de tipos a cambio de nada.
El criterio de decisión
Ante una macro con argumentos, la pregunta útil no es si funciona sino qué capacidad del preprocesador estás usando. Si la respuesta es ninguna, conviértela en función y recupera los tipos, los diagnósticos y el depurador.
Manipular texto
Convertir un argumento en literal o pegar identificadores solo lo hace el preprocesador. Es el uso legítimo por excelencia.
Capturar el contexto
__FILE__, __LINE__ y __func__ deben evaluarse en el punto de uso; una función los vería siempre en su propia definición.
Expresión constante
Una macro puede aparecer en el tamaño de un array o en un case; una llamada a función no, salvo en contextos constexpr.
Genericidad de tipo
Con _Generic o typeof, una macro atiende varios tipos donde una función exigiría una por tipo.
Cuando el veredicto sea macro, aplica además tres convenciones de higiene: nómbrala en mayúsculas y con un prefijo de proyecto, porque el preprocesador no respeta espacios de nombres y una colisión reescribe código ajeno; documenta si evalúa algún argumento más de una vez; y usa #undef para retirarla en cuanto deje de hacer falta, sobre todo si vive en una cabecera pública.
- Define
DOBLE(x)sin ningún paréntesis y encuentra dos invocaciones que den resultados distintos de los esperados, una por el argumento y otra por el contexto. - Escribe
MAXcon evaluación doble, pásale un contador y demuestra conprintfque se incrementa dos veces; repite con la versiónstatic inline. - Compila la versión de
MAXcontypeofy expresión-sentencia usando-std=c23 -Ey lee la expansión completa. - Define
REGISTRAde las tres formas de la lección y comprueba cuál compila dentro de unifconelsesin llaves. - Compila
MAXcomo macro y comostatic inlinecon-O2 -Sy compara el ensamblador generado para la misma invocación.