Peligros de las macros y el patrón do-while(0)

Las macros son instrucciones de sustitución textual procesadas por el preprocesador antes de que el compilador empiece a analizar la semántica del código. Básicamente, el preprocesador es un manipulador de texto “ciego”: no entiende de tipos, ni de ámbitos, ni de la lógica de control de C; simplemente busca patrones de texto y los reemplaza por otros. Por esta razón, aunque son herramientas potentes, su uso indiscriminado suele derivar en errores de lógica extremadamente difíciles de rastrear.

Cuando usas una macro que parece una función, como MAX(a, b), no estás ejecutando una llamada a función, sino que estás inyectando código directamente en el lugar de la llamada. Esto es lo que permite que las macros sean rápidas, pero también es lo que permite la doble evaluación de efectos secundarios. Si pasas i++ como argumento a una macro que evalúa ese argumento dos veces, la variable se incrementará dos veces, rompiendo la lógica del programa.

Para evitar comportamientos erráticos, debemos conocer cuándo es seguro usarlas. Las macros son ideales para constantes de preprocesador, guardas de inclusión en cabeceras (#ifndef), detección de características del compilador (#ifdef) y, muy importante, para tareas de logging donde necesitamos capturar la información de __FILE__ o __LINE__ en el punto exacto de la llamada. Fuera de estos casos, para implementar lógica que requiere tipos o seguridad, lo correcto es usar funciones static inline [C99] o, en casos de despacho por tipo, _Generic [C11].

Si intentas usar una macro que contiene múltiples instrucciones sin un envoltorio adecuado, puedes causar el error de consumo de punto y coma (semicolon swallowing). Si una macro se expande en dos líneas de código y la usas dentro de un bloque if sin llaves, el punto y coma de la segunda instrucción podría “romper” la estructura del if-else, dejando al else huérfano y provocando un error de sintaxis o, peor aún, un comportamiento lógico inesperado.

#include <stdio.h>

/* Peligro: Doble evaluación. Si 'a' tiene un efecto secundario, se ejecutará dos veces. */
#define MAX_BAD(a, b) ((a) > (b) ? (a) : (b))

/* Peligro: Múltiples sentencias sin envoltorio. Rompe estructuras if-else. */
#define LOG_BAD(msg) func_a(msg); func_b(msg)

/* Solución: El idiom 'do { ... } while(0)'. Convierte la macro en una sentencia única segura. */
#define LOG_SAFE(msg) do { \
    func_a(msg); \
    func_b(msg); \
} while (0)

/* Alternativa moderna: 'static inline' ofrece seguridad de tipos y es debugueable. */
static inline int max_safe(int a, int b) {
    return (a > b) ? a : b;
}

void func_a(const char *msg) { printf("[LOG A] %s\n", msg); }
void func_b(const char *msg) { printf("[LOG B] %s\n", msg); }

int main(void) {
    int x = 10;
    int y = 20;

    // 1. Demostración de doble evaluación
    // Se espera que 'x' sea 11, pero MAX_BAD(x++, y) incrementará 'x' dos veces.
    int res_max = MAX_BAD(x++, y);
    printf("MAX_BAD -> res: %d, x: %d\n", res_max, x);

    // 2. Demostración de error de estructura (Semicolon swallowing)
    // La siguiente línea daría error de compilación (else huérfano) si no se arregla.
    /*
    if (res_max > 0)
        LOG_BAD("Error detectado"); 
    else
        printf("Esto es inaccesible o causará error\n");
    */

    // 3. Uso correcto con el patrón do-while(0)
    if (res_max > 0)
        LOG_SAFE("Error seguro"); // El punto y coma aquí es necesario y seguro
    else
        printf("Ahora el flujo es predecible\n");

    // 4. Uso de la alternativa segura
    printf("max_safe -> %d\n", max_safe(10, 20));

    return 0;
}

Análisis del código

En el ejemplo anterior, MAX_BAD(a, b) expande el argumento x++ dos veces en la expresión ternaria. Debido a esto, la variable x termina valiendo 12 en lugar de 11, un error que el compilador no detectará porque es semánticamente válido, aunque lógicamente incorrecto.

La macro LOG_BAD(msg) es especialmente peligrosa. Si la usas en un if (condición) LOG_BAD("..."); else ..., el preprocesador expandirá la instrucción de forma que el else quede desconectado del if original. El patrón do { ... } while(0) en LOG_SAFE(msg) soluciona esto: obliga al uso de un punto y coma al final de la expansión y empaqueta múltiples sentencias en una única expresión que el analizador sintáctico puede tratar como una unidad atómica, permitiendo que el else se asocie correctamente con el if.

Finalmente, la función max_safe ilustra el enfoque preferido en C moderno. Al ser static inline, el compilador puede optimizar la llamada de la misma forma que una macro, pero con la ventaja de que el tipo de los argumentos se verifica y puedes usar un depurador para entrar en la función.

El error frecuente

Un error clásico de ámbito y sintaxis ocurre al intentar definir macros que agrupan sentencias usando llaves {} sin el patrón do-while(0). Fíjate en este ejemplo:

#define SWAP(a, b) { int tmp = a; a = b; b = tmp; }

if (condicion)
    SWAP(x, y); // El ';' aquí termina el bloque del 'if'
else
    printf("Esto causará un error de compilación: 'else' sin 'if' previo");

Al expandirse, el código queda como if (condicion) { ... }; else .... Ese punto y coma extra después de la llave de cierre del SWAP termina la estructura if, dejando al else huérfago. Si usas herramientas como AddressSanitizer, no detectarán esto porque no es un error de memoria, sino un error de estructura de control. Siempre que una macro contenga más de una sentencia, utiliza do { ... } while(0).

106

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio