El preprocesador a fondo
Macros y sus trampas, #embed para incrustar binarios, X-macros para generar código, y _Generic para funciones polimórficas. La metaprogramación que sí existe en C.
El preprocesador es una máquina de sustituir texto que corre antes de compilar. Poderosa y peligrosa a partes iguales: mal usada genera bugs imposibles; bien usada, habilita la única metaprogramación que C tiene. Con C23 gana herramientas modernas como #embed.
- Macros de objeto y de función, y sus trampas.
- Include guards y compilación condicional.
#embed: incrustar archivos en el binario.- X-macros y
_Generic.
Macros y sus trampas
Una macro sustituye texto, sin entender C. Eso causa los bugs clásicos:
#define CUADRADO(x) x * x
CUADRADO(2 + 3) // → 2 + 3 * 2 + 3 = 11, ¡no 25!
#define CUADRADO(x) ((x) * (x)) // paréntesis: → ((2+3)*(2+3)) = 25
CUADRADO(i++) // → ((i++)*(i++)): i++ dos veces, ¡UB!
Si escribes una macro tipo función: (1) envuelve cada parámetro y toda la macro en paréntesis: #define M(x) ((x)*(x)). (2) Nunca pases expresiones con efectos secundarios (i++, llamadas) — la macro puede evaluarlas varias veces. (3) Cuando puedas, prefiere una función static inline: es más segura (evalúa una vez, respeta tipos) y con -O2 es igual de rápida. Reserva las macros para lo que una función no puede hacer: manipular el propio texto del código.
Include guards y compilación condicional
Para que un header no se incluya dos veces (error de redefinición), protégelo:
#ifndef MI_HEADER_H
#define MI_HEADER_H
// … contenido …
#endif
// alternativa moderna y más simple:
#pragma once
La compilación condicional adapta el código a la plataforma o a opciones de build:
#ifdef __linux__
// código específico de Linux
#elif defined(_WIN32)
// código de Windows
#endif
#if __STDC_VERSION__ >= 202311L
// usar features de C23
#endif
#embed: incrustar archivos (C23)
Una de las mejores novedades de C23. #embed incrusta el contenido de un archivo directamente como bytes, sin herramientas externas:
// antes: un script convertía logo.png en un array de C
// C23: directo
const unsigned char logo[] = {
#embed "logo.png"
};
const size_t logo_size = sizeof logo;
Antes de C23, incrustar un recurso (una fuente, un icono, un shader, datos de arranque) en el binario requería un paso de build extra: un script o herramienta que leía el archivo y escupía un array de C gigante. Frágil, lento y una dependencia más. #embed lo hace el preprocesador, nativo y portable. Para firmware, juegos y binarios autocontenidos —donde quieres un único ejecutable sin archivos externos— es una mejora enorme de ergonomía. Es C23 modernizando una necesidad real de la programación de sistemas.
X-macros: generar código
Un patrón ingenioso: defines una lista una sola vez y la “reproduces” en varios contextos (declaraciones, strings, tablas), manteniéndolos sincronizados:
#define COLORES \
X(ROJO, "rojo") \
X(VERDE, "verde") \
X(AZUL, "azul")
enum Color { // genera el enum
#define X(id, str) id,
COLORES
#undef X
};
const char *nombre(enum Color c) { // genera la tabla de nombres
switch (c) {
#define X(id, str) case id: return str;
COLORES
#undef X
}
return "?";
}
Añadir un color en un solo sitio actualiza el enum y la tabla. Imposible que se desincronicen.
_Generic: polimorfismo en compilación
_Generic (C11) elige una expresión según el tipo de su argumento — la base de macros que funcionan con varios tipos:
#define abs_gen(x) _Generic((x), \
int: abs, \
long: labs, \
double: fabs \
)(x)
abs_gen(-5); // llama a abs
abs_gen(-5.0); // llama a fabs
Combinado con typeof (nivel 4), habilita utilidades genéricas seguras sin C++.
- Escribe una macro con las trampas (
x*x) y arréglala con paréntesis; luego conviértela enstatic inline. - Protege un header con
#pragma once. - Usa
#embedpara incrustar un archivo pequeño como array de bytes. - Implementa una lista con X-macros que genere un enum y su tabla de nombres.
- Escribe una macro con
_Genericque despache según el tipo.