_Generic: sobrecarga en C
Selección por tipo resuelta en tiempo de compilación: cómo funciona realmente la expresión _Generic, cómo construir macros que se comportan como funciones genéricas de verdad, y las trampas que arruinan el primer intento de todo el mundo.
C no tiene sobrecarga de funciones y probablemente nunca la tendrá: dos funciones con el mismo nombre romperían el enlazado por nombre plano en el que descansa todo su modelo de compilación separada. Lo que sí tiene desde C11, y casi nadie usa bien, es _Generic: una expresión que elige entre varias alternativas según el tipo de otra expresión, resuelta enteramente por el compilador y con coste nulo en ejecución. No es sobrecarga, pero permite construirla donde importa.
- Entender la semántica exacta de
_Generic: qué se evalúa, qué tipo se compara y cuándo falla. - Construir macros que se comporten como funciones genéricas, sin doble evaluación.
- Reconocer las trampas clásicas: ramas no elegidas, alias de tipo colisionantes y decaimiento.
- Delimitar hasta dónde llega el mecanismo y qué sigue siendo imposible en C.
Selección por tipo en tiempo de compilación
La forma es una expresión, no una sentencia: una expresión de control entre paréntesis y una lista de asociaciones tipo-resultado.
#define nombre_del_tipo(x) _Generic((x), \
int: "int", \
unsigned: "unsigned", \
double: "double", \
char *: "cadena", \
default: "otro")
printf("%s\n", nombre_del_tipo(3)); /* int */
printf("%s\n", nombre_del_tipo(3.0)); /* double */
printf("%s\n", nombre_del_tipo("hola")); /* cadena */
Cuatro reglas gobiernan todo el mecanismo y conviene memorizarlas juntas.
Primera: la expresión de control no se evalúa. Solo se examina su tipo. Puedes escribir ahí una llamada a función, una división por cero o la dereferencia de un puntero nulo y nada de eso ocurre en ejecución.
Segunda: el tipo que se compara es el que resulta tras la conversión de lvalue, más el decaimiento de arrays y funciones a puntero. Esto tiene dos consecuencias que sorprenden: los calificadores de nivel superior se descartan, así que un objeto const int casa con la asociación int; y un literal de cadena, que en C tiene tipo array de char, casa con char * y no con const char *.
Tercera: dos asociaciones no pueden nombrar tipos compatibles. Escribir a la vez una rama para unsigned long y otra para size_t es una violación de restricción en toda plataforma donde ambos sean el mismo tipo, porque typedef no crea tipos nuevos —exactamente lo que vimos en la lección anterior.
Cuarta: si ningún tipo casa y no hay rama default, el programa no compila. Eso no es un defecto, es la propiedad más valiosa del mecanismo: te permite declarar conjuntos cerrados de tipos aceptados y convertir un uso indebido en un error de compilación.
flowchart TB
A[Expresion de control] --> B[Conversion de lvalue y decaimiento]
B --> C[Tipo resultante]
C --> D{Casa con alguna asociacion}
D -->|Si| E[Esa rama es el resultado]
D -->|No, hay default| F[La rama default es el resultado]
D -->|No, sin default| G[Error de compilacion]
E --> H[Codigo generado: solo esa rama]
F --> HMacros que se comportan como funciones
El primer intento de casi todo el mundo pone expresiones completas en cada rama, y es el intento equivocado:
/* MAL: la doble evaluacion de la macro clasica sigue ahi */
#define maximo(a, b) _Generic((a), \
int: ((a) > (b) ? (a) : (b)), \
double: ((a) > (b) ? (a) : (b)))
int i = 0;
int m = maximo(i++, 5); /* i se incrementa dos veces */
La construcción correcta invierte la relación: _Generic no selecciona el cálculo, selecciona la función que lo hace, y la llamada se escribe una sola vez fuera de la selección. Como una función evalúa cada argumento exactamente una vez y comprueba los tipos de sus parámetros, todos los defectos de la macro desaparecen de golpe.
static inline int max_i(int a, int b) { return a > b ? a : b; }
static inline double max_d(double a, double b) { return a > b ? a : b; }
static inline long max_l(long a, long b) { return a > b ? a : b; }
#define maximo(a, b) _Generic((a), \
int: max_i, \
long: max_l, \
double: max_d)((a), (b))
Ahora maximo(i++, 5) incrementa i una vez, el compilador verifica los argumentos como en cualquier llamada, y con optimizaciones activadas la función se inserta en el punto de uso: el despacho desaparece por completo del código máquina. Este idioma —seleccionar un designador de función y llamarlo fuera— es el que usa la biblioteca estándar en tgmath.h para que sqrt funcione con float, double y long double, y es el único que deberías escribir.
El patrón se extiende con naturalidad a tipos propios, que es donde de verdad rinde:
typedef struct { float x, y; } Vec2;
typedef struct { float x, y, z; } Vec3;
static inline float vec2_norma(Vec2 v);
static inline float vec3_norma(Vec3 v);
#define norma(v) _Generic((v), Vec2: vec2_norma, Vec3: vec3_norma)((v))
Sin rama default, pasar un Vec4 que aún no soportas es un error de compilación con nombre y línea, no una conversión silenciosa.
Aunque solo una asociación aporta el resultado, todas deben ser expresiones válidas y con tipos correctos. Por eso esto falla: _Generic((p), int *: *p, default: 0) cuando p es un int. La rama descartada contiene *p, que no tiene sentido para un entero, y el compilador la rechaza antes de decidir nada. Es la razón técnica de que seleccionar funciones funcione y seleccionar expresiones no: un identificador de función siempre es una expresión válida, se elija o no. Cuando de verdad necesites expresiones heterogéneas, encapsula cada una en su propia función o interpón una conversión que haga la rama muerta inofensiva.
Las trampas que arruinan el primer intento
Además de la anterior, hay cuatro que aparecen sin falta en cuanto el uso deja de ser un ejemplo de manual.
Los alias colisionan. Ya está dicho, pero merece repetirse porque el error es desconcertante: uint32_t, unsigned y unsigned int pueden ser el mismo tipo, y ponerlos en la misma lista rompe la compilación en unas plataformas y no en otras. Enumera tipos fundamentales, no alias, salvo que sepas exactamente a qué se resuelven.
Los enteros pequeños no llegan enteros. Un argumento de tipo char o short no se promociona antes de la selección: _Generic mira el tipo tal cual, así que necesitas ramas explícitas para char, signed char, unsigned char, short y unsigned short. Y recuerda que char es un tipo distinto de los otros dos, no un sinónimo de ninguno.
#define ancho(x) _Generic((x), \
char: 1, \
signed char: 1, \
unsigned char: 1, \
short: 2, \
unsigned short: 2, \
int: 4)
char c = 'a';
ancho(c); /* 1: casa con 'char', nunca con 'signed char' */
ancho(c + 0); /* 4: la suma promociona a int ANTES de la seleccion */
Ese segundo caso es el que más desconcierta: basta una operación aritmética trivial en el argumento para que el tipo que ve _Generic deje de ser el que escribiste.
Los punteros son estrictos. int * y const int * son tipos diferentes y necesitan ramas separadas; solo se descartan los calificadores del nivel más externo, nunca los del apuntado.
Las macros con coma sin proteger explotan. Si un argumento contiene una coma no encerrada en paréntesis, el preprocesador lo parte antes de que _Generic vea nada. Envuelve siempre los parámetros en paréntesis dentro de la macro.
C23 introduce nullptr con su propio tipo, nullptr_t, declarado en stddef.h. Eso significa que ahora una macro genérica puede tener una rama exclusiva para el puntero nulo, algo imposible antes porque NULL podía ser 0 —un int— o un void * según la implementación, y la selección cambiaba de compilador a compilador. Combinado con typeof, que permite nombrar el tipo de una expresión sin inventarle un alias, C23 hace de _Generic una herramienta bastante más predecible que en C11.
Hasta dónde llega y dónde acaba
_Generic es despacho estático sobre un conjunto cerrado y escrito a mano de tipos. Esa frase contiene a la vez toda su potencia y todas sus fronteras.
No hay selección por tipo de retorno; no hay selección por número de argumentos, aunque contar argumentos con __VA_OPT__ y macros auxiliares permite simularlo; no hay conversiones definidas por el usuario que amplíen las coincidencias; y, sobre todo, no hay extensibilidad: un tercero que defina su propio tipo no puede añadirlo a tu lista sin editar tu macro. Frente a las plantillas de C++ o los trait de Rust, que se abren a implementaciones futuras, _Generic es un switch sobre tipos que alguien tuvo que escribir entero de antemano.
Tampoco compone: _Generic no puede razonar sobre “cualquier puntero” ni sobre “cualquier tipo aritmético”; solo sobre los tipos que enumeres uno a uno. En la práctica eso lo confina a listas cortas y estables, y explica que su uso mayoritario sea en cabeceras de biblioteca —tgmath.h, envoltorios de atómicos, capas de compatibilidad— y no en el código de aplicación.
Lo que _Generic introduce en C no es azúcar para escribir menos: es la primera construcción del lenguaje en la que el tipo de una expresión se comporta como un valor sobre el que se puede decidir. Hasta C11, los tipos existían solo para que el compilador comprobase lo que escribías; con _Generic, el tipo pasa a ser una entrada que dirige qué código se genera, y con ello aparece un pequeño lenguaje de programación que se ejecuta durante la traducción y cuyo resultado es el programa. Esa idea —computación dirigida por tipos en tiempo de compilación— es la misma que sostiene las plantillas de C++, los trait de Rust y los genéricos de cualquier lenguaje moderno con abstracción de coste cero; C la ofrece en su forma más desnuda y honesta: sin inferencia, sin apertura, sin instanciación implícita, un switch explícito sobre tipos. Y precisamente por desnuda enseña mejor que ninguna otra la lección de fondo: la abstracción de coste cero no consiste en que el compilador sea listo, sino en trasladar una decisión del momento de la ejecución al momento de la traducción, donde ya no cuesta nada porque cuando el programa corre la decisión hace tiempo que está tomada. Cada vez que escribes una lista de asociaciones estás pagando en escritura lo que un lenguaje con genéricos te da a cambio de un sistema de tipos mucho más complejo, y ver ese intercambio en su forma más cruda es lo que permite juzgar con criterio a los lenguajes que lo ocultan.
- Escribe
nombre_del_tipo(x)con ramas parachar,short,int,long,float,doubleychar *, y comprueba con qué rama casa una variableconst int. - Demuestra con un contador que la versión de
maximoque selecciona expresiones evalúa dos veces y la que selecciona funciones evalúa una. - Provoca a propósito el error de asociaciones compatibles poniendo
unsigned longysize_ten la misma lista. Lee el diagnóstico. - Construye
imprimir(x)sobre dos tipos propios, sindefault, y verifica que un tercer tipo produce un error de compilación en vez de una conversión silenciosa. - Añade una rama para
nullptr_ty explica por qué esa distinción era imposible antes de C23.