La palabra clave _Generic [introducida en C11] es una expresión de selección de tipos en tiempo de compilación. No es una estructura de control de flujo como if o switch, ya que no se evalúa durante la ejecución del programa, sino durante la fase de compilación. Su función es examinar el tipo estático de una expresión de control y, basándose en ese tipo, elegir una de las expresiones proporcionadas en una lista de asociaciones.
El mecanismo interno de _Generic es una selección de “matching” de tipos. Cuando el compilador encuentra esta expresión, inspecciona el tipo de la expresión de control (aplicando primero las reglas de integer promotion y usual arithmetic conversions) y busca una coincidencia en la lista. Si encuentra el tipo, la expresión asociada es la que se integra en el código; si no, salta a la cláusula default.
Este mecanismo es fundamental para implementar polimorfismo de tipo en C mediante macros. Un caso de uso real es la librería <tgmath.h>, que permite llamar a funciones como sin(x) de forma “genérica”: si x es double, el compilador expande la llamada a sin, si es float, a sinf, y si es float complex, a la versión compleja. Sin _Generic, el programador tendría que llamar manualmente a la versión correcta de cada función para evitar conversiones implícitas que degradan la precisión.
Deberías usar _Generic cuando necesites crear APIs que operen sobre múltiples tipos de forma segura (type-safe), evitando el uso de void* y los peligrosos casts manuales. Sin embargo, ten cuidado: si intentas usarlo basándote en el valor de una variable (tipo de dato determinado en runtime, como un valor que viene de un archivo) o si el tipo tiene calificadores const que no hayas contemplado, la lógica fallará o caerá en el default. Lo que se rompe principalmente es la semántica de tu macro si el programador asume que _Generic es un “introspeccionador” de memoria en tiempo de ejecución; _Generic es ciego a lo que hay en la memoria, solo le importa la etiqueta del tipo en la tabla de símbolos del compilador.
#include <stdio.h>
/* Funciones de implementación con tipos específicos para evitar ambigüedad */
void print_int(int x) { printf("Entero: %d\n", x); }
void print_float(float x) { printf("Float: %f\n", x); }
void print_double(double x) { printf("Double: %lf\n", x); }
void print_string(const char *s) { printf("Cadena: %s\n", s); }
void print_unknown(void *x) { printf("Tipo no soportado o void*\n"); }
/**
* Macro genérica para dispatch de tipos.
* El _Generic devuelve un puntero a la función seleccionada,
* y el (x) al final realiza la llamada con el argumento original.
*/
#define smart_print(x) _Generic((x), \
int: print_int, \
float: print_float, \
double: print_double, \
char*: print_string, \
const char*: print_string, \
default: print_unknown \
)(x)
int main(void) {
int a = 42;
float b = 3.14f;
double c = 2.71828;
const char *d = "C11 es potente";
char e[] = "No soy const";
/* El compilador elige la función en tiempo de compilación */
smart_print(a);
smart_print(b);
smart_print(c);
smart_print(d);
smart_print(e);
/* Un literal decimal es double por defecto */
smart_print(1.0);
/* Error intencionado de tipo para demostrar el 'default' */
int *ptr = &a;
smart_print(ptr);
return 0;
}
Desglose del ejemplo
En el código anterior, la macro smart_print(x) es el núcleo del dispatch. Observa la expresión _Generic((x), ...). El primer argumento es la expresión de control. Es crucial notar que (x) se evalúa para determinar su tipo, pero no se evalúa su valor para decidir el flujo, lo que garantiza eficiencia absoluta.
Cuando llamamos a smart_print(a), el compilador analiza a, ve que es de tipo int y selecciona la dirección de la función print_int. La macro se expande efectivamente a print_int(a). Lo mismo ocurre con c y print_double.
Un detalle sutil es la distinción entre char* y const char*. En C, un literal como "Hola" tiene tipo const char[5], que al pasarse por valor suele evaluarse como const char*. Si no hubiéramos incluido explícitamente la asociación const char*: print_string en el _Generic, la llamada a un literal caería en print_unknown debido a la estricta coincidencia de tipos de C.
En la línea smart_print(1.0), el literal 1.0 es tratado por el compilador como double. Por lo tanto, se selecciona print_double. Si escribiéramos 1.0f, el compilador seleccionaría print_float. Esta capacidad de distinguir entre precisiones de punto flotante mediante la sintaxis del literal es lo que permite que las bibliotecas matemáticas funcionen correctamente.
Finalmente, al pasar ptr (un int*), el compilador busca una coincidencia para el tipo int*. Como no está en nuestra lista, se ejecuta la rama default, llamando a print_unknown.
El error frecuente
Un error muy común con _Generic ocurre al intentar manejar cadenas de texto sin considerar los calificadores de tipo const.
/* Código con error de lógica de tipos */
#define print_only_char_ptr(x) _Generic((x), \
char*: print_string, \
default: print_unknown \
)(x)
int main(void) {
char *s = "Texto literal"; // Error: es const char*
print_only_char_ptr(s); // Imprimirá "Tipo no soportado"
}
Aunque s parece un char*, los literales de cadena son inmutables y su tipo real es const char *. El _Generic es estricto: si no defines la asociación para const char*, no encontrará la coincidencia. Además, _Generic no puede resolver tipos en tiempo de ejecución; si tienes un void *p que apunta a un int, _Generic solo verá void * y no podrá “mirar” dentro de la dirección para decidir qué función llamar.
N° 103