_Generic en C11: Selección de tipos en tiempo de compilación

_Generic es una palabra clave introducida en [C11] que permite realizar una selección de tipos en tiempo de compilación mediante una estructura de control basada en la naturaleza del tipo de una expresión. No es una función, sino una expresión que, a diferencia de un switch o un if, no evalúa la expresión de control; simplemente consulta su tipo en la tabla de símbolos del compilador durante la fase de análisis.

Esto permite implementar una forma de sobrecarga de funciones (overloading) mediante macros que es segura en cuanto al tipo (type-safe), evitando los peligrosos castings de void* o el uso de macros genéricas sin tipo que pueden llevar a errores de precisión o de desbordamiento. Su objetivo es permitir que una misma interfaz (el nombre de una macro) despache a diferentes implementaciones especializadas según si el argumento es un int, float, double o un puntero, todo sin incurrir en el coste de ejecución de una tabla de funciones virtual (vtable) o de una decisión en tiempo de ejecución.

La sintaxis se compone de una expresión de control, una serie de asociaciones de tipo-expresión y, opcionalmente, una cláusula default. El compilador analiza el tipo de la expresión de control y, si encuentra una coincidencia, devuelve el resultado de la expresión asociada. Si no hay coincidencia y existe un default, se utiliza este último.

#include <stdio.h>

/* Funciones especializadas para cada tipo de dato */
static inline int    _sq_int(int x)       { return x * x; }
static inline float  _sq_float(float x)   { return x * x; }
static inline double _sq_double(double x) { return x * x; }

/* 
 * Macro genérica 'square'.
 * 1. _Generic((x), ...) inspecciona el tipo de 'x'.
 * 2. Selecciona el identificador de la función (ej. _sq_int).
 * 3. El (x) final aplica la función seleccionada al argumento original.
 */
#define square(x) _Generic((x), \
    int:    _sq_int,            \
    float:  _sq_float,          \
    double: _sq_double          \
)(x)

int main(void) {
    int    i = 5;
    float  f = 3.14f;
    double d = 2.71828;

    /* La resolución ocurre totalmente en tiempo de compilación */
    printf("Cuadrado de %d (int):    %d\n", i, square(i));
    printf("Cuadrado de %f (float): %f\n", f, square(f));
    printf("Cuadrado de %lf (double):%lf\n", d, square(d));

    return 0;
}

Análisis del código

En el ejemplo anterior, la macro square(x) utiliza el patrón de despacho de funciones. La expresión dentro de _Generic, (x), es la expresión de control. Es vital entender que el compilador no ejecuta x; solo extrae la información de su tipo de la tabla de símbolos.

Cuando invocamos square(i), donde i es de tipo int, la asociación int: _sq_int se activa. En este caso, _Generic no devuelve el valor de i, sino el puntero a la función _sq_int. Por tanto, la macro se expande efectivamente a _sq_int(i). Es por esto que el paréntesis final (x) es obligatorio: sin él, la macro simplemente devolvería un punero a la función sin ejecutarla.

Si pasáramos un literal como 2.5, el compilador lo trataría como double (según la convención de constantes en C), seleccionando _sq_double. Si pasáramos 2.5f, la etiqueta float capturaría la llamada. Esta distinción es lo que hace que las implementaciones sean type-safe: no hay conversiones implícitas de tipos que puedan perder precisión durante la llamada a la función, ya que el compilador elige la firma exacta que coincide con el tipo de entrada.

El uso de static inline en las funciones especializadas es una práctica recomendada para permitir que el compilador realice un inlining agresivo, eliminando cualquier residuo de la llamada a la función y dejando solo la operación matemática, lo que iguala el rendimiento de una macro primitiva pero con la seguridad de un sistema de tipos fuerte.

El error frecuente

Un error sutil y peligroso ocurre cuando la expresión de control de la macro contiene efectos secundarios y se intenta usar dentro de la macro de forma redundante.

#define WRONG_MACRO(x) _Generic((x), int: x)(x)

int i = 0;
WRONG_MACRO(i++);

En WRONG_MACRO(i++), el compilador primero analiza el tipo de la expresión de control i++. El tipo es int, por lo que selecciona la expresión asociada x (que es i++). El _Generic se evalúa y, como la asociación es i++, se ejecuta el incremento: i pasa a valer 1. Sin embargo, la macro termina con (x), lo que provoca una segunda evaluación de i++. Como resultado, i termina valiendo 2 y el valor devuelto es errático.

Este comportamiento es un ejemplo de cómo _Generic puede interactuar de forma inesperada con el modelo de evaluación de C si el programador no distingue claramente entre la expresión de control (usada para el despacho) y la expresión seleccionada (usada para el cómputo). Para evitarlo, la expresión de control debe ser siempre una variable simple o un valor constante.

73

Dejar un comentario

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

Scroll al inicio