Una macro variádica es una directiva de preprocesado que permite recibir un número indeterminado de argumentos, de forma similar a cómo las funciones pueden usar parámetros elípticos (...). Su propósito principal no es sustituir a las funciones, sino actuar como un generador de código en tiempo de preprocesado. Esto es vital para crear wrappers de depuración que inyecten automáticamente metadatos como el nombre del archivo (__FILE__) o la línea exacta (__LINE__), algo que sería extremadamente tedioso o imposible de lograr con funciones normales sin pasar esos datos manualmente.

El núcleo de esta funcionalidad es el identificador __VA_ARGS__ [C99], que el preprocesador expande sustituyéndolo por la lista de argumentos que siguen al primer parámetro fijo. Sin embargo, el uso de __VA_ARGS__ en C99 estándar tiene una debilidad de diseño: la “coma huérfana”. Si defines una macro como #define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__) y la invocas con un solo argumento (LOG("Hola")), el preprocesador expandirá el código como fprintf(stderr, "Hola", );. Esa coma extra antes del paréntesis de cierre es un error de sintaxis que impedirá la compilación.

Para mitigar esto, los compiladores como GCC y Clang implementaron una extensión no estándar que permitía eliminar la coma si la lista de argumentos estaba vacía mediante el uso del operador ## antes de __VA_ARGS__. Pero con la llegada de C23 [C23], el estándar finalmente aborda este problema de forma elegante con la directiva __VA_OPT__(...). Esta nueva instrucción solo expande su contenido (en nuestro caso, la coma) si __VA_ARGS__ no está vacío.

Debes usar estas macros cuando necesites metaprogramación para mejorar el diagnóstico de errores o la legibilidad del código mediante macros de logging. Si intentas realizar estas tareas con funciones reales utilizando va_list, el proceso será mucho más complejo, perderás la capacidad de usar los operadores de preprocesador de forma directa y tendrás un impacto mayor en el rendimiento al gestionar la pila en tiempo de ejecución en lugar de dejar que el compilador optimice la expansión de texto.

#include <stdio.h>

/* 
 * Macro de depuración robusta compatible con C23.
 * Usamos do { ... } while (0) para asegurar que la macro se comporte 
 * como una única sentencia atómica, permitiendo su uso seguro en estructuras if/else.
 */
#define DEBUG(fmt, ...) \
    do { \
        fprintf(stderr, "[DEBUG] [%s:%d] " fmt "\n", \
                __FILE__, __LINE__, \
                __VA_OPT__(,) __VA_ARGS__) \
    } while (0)

/* 
 * Macro de información general que permite pasar argumentos adicionales 
 * de forma segura gracias a __VA_OPT__.
 */
#define INFO(fmt, ...) \
    do { \
        fprintf(stderr, "[INFO] " fmt "\n", \
                __VA_OPT__(,) __VA_ARGS__) \
    } while (0)

int main(void) {
    int error_code = 500;
    const char *componente = "auth_module";

    /* Caso 1: Uso con múltiples argumentos variádicos */
    DEBUG("Estado del sistema: %s, error: %d", componente, error_code);

    /* Caso 2: Uso sin argumentos adicionales (el caso que rompía C99 estándar) */
    INFO("Servidor iniciado correctamente");

    /* Caso 3: Uso con un solo argumento adicional */
    DEBUG("Conexión establecida con %s", componente);

    return 0;
}

En el ejemplo anterior, la magia ocurre en la expresión __VA_OPT__(,) __VA_ARGS__. Cuando llamas a DEBUG con argumentos como "Estado del sistema..." y error_code, __VA_ARGS__ contiene los tokens correspondientes. En este escenario, __VA_OPT__(,) detecta que hay contenido y se expande como una coma ,, resultando en una sintaxis de función printf perfectamente válida.

Cuando llamas a INFO("Servidor iniciado correctamente"), __VA_ARGS__ está vacío. En este caso, __VA_OPT__(,) no expande absolutamente nada (ni la coma ni nada más), evitando que la instrucción finalice con una coma sobrante. Es importante notar que el uso de do { ... } while (0) en las definiciones de las macros no es una sugerencia estética; es una técnica necesaria para que, si utilizas la macro dentro de un if sin llaves, la sintaxis sea consistente y no se produzcan errores de expansión que afecten a la lógica de control.

El error frecuente ocurre cuando intentas implementar estas macros en entornos de legado o siguiendo estrictamente C99 sin conocer la extensión de GCC.

/* Error común en C99 estándar */
#define LOG_ERROR(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)

int main(void) {
    // Esto fallará en un compilador estricto de C99
    LOG_ERROR("Fallo crítico"); 
    // Se expande a: fprintf(stderr, "Fallo crítico", ); <--- Error de sintaxis
    return 0;
}

Este error no es un problema de memoria ni un comportamiento indefinido que detectaría Valgrind; es un error de sintaxis del preprocesador. El compilador fallará durante la fase de análisis antes siquiera de intentar generar código máquina.

104