wandres.dev
TÁCTICO · El preprocesador a fondo

Stringify y token pasting: los operadores # y ##

Los dos únicos operadores del preprocesador convierten un argumento en literal de cadena y pegan dos tokens en uno. Esta lección precisa sus reglas exactas, explica por qué siempre necesitan una macro auxiliar de indirección, y muestra cómo generar familias enteras de funciones tipadas sin escribirlas a mano.

⏱ 19 min

El preprocesador tiene exactamente dos operadores propios, y ninguno de los dos calcula nada: # convierte el texto de un argumento en un literal de cadena y ## funde dos tokens en uno solo. Parecen curiosidades sintácticas y son en realidad la puerta a la única metaprogramación de C, porque permiten que el nombre de un identificador se construya durante la traducción en lugar de estar escrito en el fichero. Todo lo que viene después —trazas que se autodocumentan, familias de funciones tipadas, X-macros— descansa sobre ellos.

🎯 Al terminar esta lección sabrás
  • Aplicar el operador # conociendo su normalización de espacios y su escape automático.
  • Aplicar el operador ## y reconocer cuándo produce un token inválido.
  • Explicar por qué ambos exigen una macro intermedia para expandir sus argumentos.
  • Generar familias de declaraciones y funciones a partir de un único patrón parametrizado.

El operador # convierte un argumento en literal

Colocado delante del nombre de un parámetro, dentro del cuerpo de una macro de función, # sustituye ese parámetro por un literal de cadena que contiene el texto original del argumento tal como se escribió en el punto de uso.

#define IMPRIME_EXPR(e)  printf("%s = %d\n", #e, (e))

int a = 3, b = 4;
IMPRIME_EXPR(a * b + 1);   /* imprime: a * b + 1 = 13 */

Tres reglas gobiernan el resultado y conviene tenerlas juntas. Primera: los espacios en blanco interiores se normalizan a un solo espacio, y los del principio y el final se eliminan; el literal no reproduce tu formato, reproduce la secuencia de tokens. Segunda: si el argumento contiene comillas o barras invertidas, se escapan automáticamente, de modo que el literal resultante siempre es válido. Tercera: # solo puede preceder a un parámetro de una macro de función; delante de cualquier otra cosa es un error de compilación, salvo al principio de línea, donde sigue siendo el signo de directiva.

Hay una restricción que sorprende y que no es un capricho: el resultado tiene que ser un literal de cadena legal. Si el argumento acaba en una barra invertida suelta, la expansión produce un literal mal formado y el comportamiento queda sin definir.

El uso canónico es la aserción que se explica a sí misma, un patrón que aparece en toda base de código seria porque combina las dos capacidades exclusivas del preprocesador: el texto del argumento y el contexto léxico del punto de uso.

#define COMPRUEBA(cond)                                            \
    do {                                                           \
        if (!(cond)) {                                             \
            fprintf(stderr, "%s:%d: fallo la condicion: %s\n",     \
                    __FILE__, __LINE__, #cond);                    \
            abort();                                               \
        }                                                          \
    } while (0)

Ninguna función puede hacer esto: recibiría el valor de la condición, ya reducido a cero o uno, y habría perdido para siempre tanto el texto que la produjo como la línea en la que estaba escrita.

El operador ## pega tokens

Colocado entre dos elementos del cuerpo, ## elimina el espacio conceptual entre ellos y los funde en un único token de preprocesamiento. La aplicación evidente es fabricar identificadores.

#define CAMPO(pref, nombre)  pref ## _ ## nombre

int CAMPO(usuario, edad);    /* declara: int usuario_edad; */

La regla dura es que el resultado tiene que ser un token válido. Pegar 1 con e produce 1e, que no es un número legal, y el estándar declara el comportamiento indefinido; en la práctica el compilador emite un diagnóstico y se detiene, pero no cuentes con ello. Pegar dos operadores para formar uno mayor, como + con +, también queda fuera de lo garantizado.

La segunda regla es más sutil y explica la mitad de la frustración de quien empieza: los operandos de ## no se expanden antes de pegarse. Si uno de ellos es a su vez una macro, lo que se pega es su nombre, no su valor. Lo mismo ocurre con el argumento de #.

#define VERSION  7
#define UNE(a, b)  a ## b

UNE(v, VERSION)     /* produce el identificador vVERSION, no v7 */

Existe además una restricción de orden que casi nadie conoce y que aparece al combinar varias operaciones: cuando en un mismo cuerpo hay varios ##, el orden de evaluación entre ellos no está especificado. Si tu construcción depende de que se pegue primero un par concreto, encadena macros auxiliares en lugar de acumular operadores en una sola línea.

La doble indirección: la macro auxiliar obligatoria

De la regla anterior sale el idioma más importante del preprocesador. Como los argumentos se expanden antes de sustituirse cuando no participan en # ni en ##, basta interponer una macro que no haga nada más que reenviar sus argumentos: en ese reenvío se expanden, y la macro interna recibe ya el resultado.

#define STR_(x)   #x          /* aplica el operador */
#define STR(x)    STR_(x)     /* fuerza la expansion previa */

#define CAT_(a, b)  a ## b
#define CAT(a, b)   CAT_(a, b)

#define VERSION 7

STR(VERSION)        /* -> "7"    ; STR_(VERSION) daria "VERSION" */
CAT(v, VERSION)     /* -> v7     ; CAT_(v, VERSION) daria vVERSION */
CAT(tmp, __LINE__)  /* -> tmp42 si la invocacion esta en la linea 42 */

Esa última línea es el generador de nombres únicos que usan casi todos los frameworks de test y las macros de registro estático: cada invocación en un fichero produce un identificador distinto sin que el usuario tenga que inventarlo. Un aviso, sin embargo: si dos invocaciones caen en la misma línea, o si el compilador soporta __COUNTER__, conviene preferir este último, porque __LINE__ puede repetirse dentro de otra macro.

💡
C23 estandariza __VA_OPT__ y jubila un truco veterano

Las macros variádicas tenían un defecto conocido: al invocar TRAZA("hola") sin argumentos adicionales, el cuerpo printf(fmt, __VA_ARGS__) dejaba una coma colgando. Durante veinte años la solución fue la extensión de GCC , ## __VA_ARGS__, que abusaba de ## para borrar la coma anterior. C23 incorpora __VA_OPT__ al estándar: su contenido aparece si y solo si la lista variádica no está vacía, con lo que printf(fmt __VA_OPT__(,) __VA_ARGS__) es correcto, portable y dice lo que hace. Si mantienes código antiguo, sustituir el truco por __VA_OPT__ es una de las migraciones a C23 más baratas y de mayor rendimiento.

flowchart TB
A[argumento de la macro] --> B{participa en almohadilla o en pegado}
B -->|Si| C[se toma el texto literal sin expandir]
B -->|No| D[se expande primero y luego se sustituye]
C --> E[resultado inesperado si el argumento era una macro]
D --> F[resultado esperado]
E --> G[solucion: macro intermedia que solo reenvia]
G --> D
style E fill:#f38ba8,color:#11111b
style F fill:#a6e3a1,color:#11111b

Generar código repetitivo sin escribirlo

La aplicación seria de ## es fabricar familias enteras de declaraciones a partir de un patrón parametrizado por el tipo. En un lenguaje sin plantillas ni genéricos, es la única forma de tener el mismo contenedor para varios tipos sin perder el tipado en void *.

#define DECLARA_PILA(T, sufijo)                                  \
    typedef struct {                                             \
        T     *datos;                                            \
        size_t tam, cap;                                         \
    } Pila_##sufijo;                                             \
                                                                 \
    static inline void pila_##sufijo##_init(Pila_##sufijo *p) {  \
        p->datos = nullptr; p->tam = p->cap = 0;                 \
    }                                                            \
                                                                 \
    static inline bool pila_##sufijo##_push(Pila_##sufijo *p, T v)

DECLARA_PILA(int,    i);   /* Pila_i,  pila_i_init,  pila_i_push  */
DECLARA_PILA(double, d);   /* Pila_d,  pila_d_init,  pila_d_push  */

Cada expansión genera un tipo distinto con funciones distintas y comprobación de tipos completa: pasar una Pila_d a pila_i_push es un error de compilación, no una conversión silenciosa. El precio es que el depurador y los mensajes de error hablan del texto expandido, así que conviene mantener el cuerpo de estas macros pequeño y delegar la lógica de verdad en funciones normales que la macro se limite a envolver.

El segundo uso frecuente es fabricar tablas de despacho a partir de nombres, garantizando que el nombre del manejador y su entrada en la tabla nunca se desincronicen.

#define MANEJADOR(op)  [OP_##op] = manejar_##op

static void (*const tabla[])(Contexto *) = {
    MANEJADOR(SUMA), MANEJADOR(RESTA), MANEJADOR(SALTO),
};
El preprocesador es un lenguaje de programacion sin tipos cuyo dato es tu codigo

Conviene ver # y ## por lo que son en el plano conceptual: los constructores de un lenguaje de programación completo que se ejecuta durante la traducción y cuyo único tipo de dato es la secuencia de tokens de tu propio programa. El operador # es la reificación —convierte código en dato, un fragmento del programa en una cadena que el programa puede manipular en ejecución— y el operador ## es la construcción de símbolos, la capacidad de calcular un nombre en lugar de escribirlo. Esa pareja, código como dato y símbolos calculados, es exactamente la que define las macros de Lisp, la reflexión de Java o los derives de Rust; C la ofrece en su forma más primitiva y más honesta, sin ningún conocimiento de la sintaxis del lenguaje que está generando. De ahí salen a la vez su potencia y su fama: cualquier cosa que puedas escribir la puedes generar, pero como el generador no entiende gramática ni tipos, un error se manifiesta después de la sustitución, sobre un texto que no aparece en ningún fichero, y el diagnóstico apunta a una línea que tú no escribiste. La disciplina que compensa esa asimetría es siempre la misma: que la macro genere estructura —nombres, declaraciones, entradas de tabla— y que la lógica viva en funciones normales que la macro se limite a invocar. Así el generador se mantiene trivial y verificable, la parte difícil conserva el tipado y los mensajes del compilador, y usas el preprocesador para lo único que sabe hacer y nadie más puede: escribir el código que tú no quieres escribir.

⚔️ Fabrica identificadores y literales
  1. Escribe COMPRUEBA(cond) con #, __FILE__ y __LINE__, y comprueba qué imprime al pasarle una condición con comillas dentro.
  2. Demuestra la diferencia entre STR_ y STR con una macro numérica; explica el resultado en términos de la regla de no expansión.
  3. Genera nombres únicos con CAT(tmp, __LINE__), provoca a propósito una colisión poniendo dos invocaciones en la misma línea y sustituye por __COUNTER__.
  4. Expande DECLARA_PILA para dos tipos, escribe los cuerpos de push y verifica que mezclar las pilas produce un error de compilación.
  5. Reescribe una macro variádica que use el truco , ## __VA_ARGS__ empleando __VA_OPT__ y compila con -std=c23 -pedantic.