auto, constexpr y typeof: el C23 que moderniza sin traicionar
La inferencia de tipos de auto, las constantes con tipo de constexpr y la introspección de typeof y typeof_unqual: qué problema histórico resuelve cada una y dónde se detiene.
Durante treinta años, las constantes de C fueron macros sin tipo, los tipos de una expresión eran inaccesibles y repetir declaraciones larguísimas era rutina. C23 estandariza tres piezas —auto, constexpr y typeof— que resuelven exactamente esos huecos. Ninguna añade maquinaria oculta ni coste en ejecución: son las mismas garantías de siempre, expresadas por el sistema de tipos en vez de por el preprocesador.
- Usar
autopara inferir tipos y conocer sus restricciones exactas. - Sustituir macros de constantes por
constexpry saber dónde no llega. - Escribir macros seguras con
typeofytypeof_unqual. - Situar estas novedades frente a lo que C decidió deliberadamente no copiar.
auto: inferencia con letra pequeña
En C23, auto sin ningún otro especificador de tipo pasa a significar “deduce el tipo del inicializador”. La palabra clave existía desde el principio como clase de almacenamiento, pero era redundante; el comité reutilizó ese cadáver sintáctico en lugar de gastar una palabra nueva.
auto n = 42; // int
auto x = 3.14; // double
auto texto = "hola"; // char*: el array decae a puntero
const int c = 7;
auto copia = c; // int, NO const: los calificadores se descartan
Ese último caso es la letra pequeña más importante: el tipo se deduce tras la conversión de lvalue, que elimina los calificadores de nivel superior y hace decaer los arrays a puntero. auto te da una copia modificable, no un alias del original.
Las restricciones son estrictas y conviene memorizarlas: el inicializador es obligatorio, solo se permite un declarador por declaración, y no vale para parámetros de función, miembros de estructura ni tipos de retorno. Tampoco puedes referirte al objeto que estás declarando dentro de su propio inicializador.
auto a = 1, b = 2; // error: más de un declarador
auto sin_valor; // error: falta el inicializador
auto no está para ahorrar teclas en auto n = 42, donde int es más corto y más claro. Gana en tres sitios: capturar el resultado de una macro cuyo tipo depende de sus argumentos, evitar la repetición de tipos de puntero a función, y escribir cuerpos de macro que declaran temporales sin saber su tipo. Fuera de ahí, el tipo explícito casi siempre comunica mejor la intención, que es la tradición de C.
constexpr: constantes con tipo
La forma histórica de declarar una constante era #define TAM 256: texto sin tipo, sin ámbito y sin visibilidad para el depurador. La alternativa const int TAM = 256 tiene tipo pero, en C, no es una expresión constante entera: no sirve como tamaño de un array de duración estática ni como etiqueta de case.
constexpr cierra ese hueco. Define un objeto cuyo valor se conoce en compilación, con tipo, ámbito y todas las propiedades de un identificador real:
constexpr int TAM = 256;
constexpr double PI = 3.14159265358979;
constexpr auto MASCA = 0xFFu; // combina con auto: unsigned int
int buffer[TAM]; // array normal, no de longitud variable
static int tabla[TAM]; // válido también en duración estática
switch (v) {
case TAM: break; // sirve como etiqueta de case
}
Las reglas: el objeto es implícitamente const, el inicializador debe ser una expresión constante y no se admite un estrechamiento que cambie el valor. No puede tener tipo volatile ni atómico, ni ser un array de longitud variable; si es de tipo puntero, su valor debe ser una constante de puntero nulo. Para estructuras, todos los miembros deben poder inicializarse con expresiones constantes.
constexpr no llega al preprocesador. #if TAM > 128 no funciona, porque el preprocesador se ejecuta antes de que exista el concepto de identificador con tipo y sustituye por cero cualquier nombre que no conozca. Para compilación condicional siguen haciendo falta macros. Y el hueco mayor: C23 no tiene funciones constexpr. La palabra clave se aplica solo a definiciones de objetos; para calcular una constante a partir de otras sigues teniendo el operador aritmético y poco más.
typeof y typeof_unqual
typeof lleva décadas siendo una extensión de GCC en la que se apoyaban el kernel de Linux y media biblioteca del mundo. C23 la estandariza. Acepta un nombre de tipo o una expresión, y en el segundo caso la expresión no se evalúa: solo se inspecciona su tipo.
int a = 5;
typeof(a) b = 10; // int
typeof(int *) p = &a; // int*
const int c = 1;
typeof(c) d = 2; // const int: conserva los calificadores
typeof_unqual(c) e = 3; // int: los descarta, incluido _Atomic
La distinción entre las dos formas es la que decide si tu macro funciona. Cuando creas un temporal a partir de un objeto que puede ser const, volatile o atómico, quieres typeof_unqual: con typeof a secas el temporal heredaría el calificador y no podrías asignarle nada.
#define INTERCAMBIA(x, y) \
do { \
typeof_unqual(x) tmp_ = (x); \
(x) = (y); \
(y) = tmp_; \
} while (0)
Evaluar cada argumento una sola vez y usar un nombre de temporal poco probable —de ahí el guion bajo final— sigue siendo responsabilidad tuya: typeof da introspección de tipos, no higiene de macros.
flowchart LR A[define TAM 256: texto sin tipo] --> B[constexpr int TAM: valor con tipo y ambito] C[NULL ambiguo entre cero y puntero] --> D[nullptr con tipo propio] E[typeof como extension de GCC] --> F[typeof y typeof_unqual en el estandar] G[Repetir tipos largos a mano] --> H[auto deduce del inicializador] style B fill:#a6e3a1,color:#11111b style D fill:#a6e3a1,color:#11111b style F fill:#a6e3a1,color:#11111b style H fill:#a6e3a1,color:#11111b
Qué resuelven y dónde se detienen
Las cuatro flechas del diagrama cuentan la misma historia: cada novedad sustituye un truco del preprocesador por una construcción con tipo. constexpr reemplaza las macros de constantes, nullptr desambigua NULL, typeof legaliza una extensión que ya era imprescindible y auto elimina la repetición mecánica. Ninguna genera código adicional ni introduce indirección: el binario resultante es el mismo.
constexpr
Sustituye a #define para constantes. Aporta tipo, ámbito, símbolo visible en el depurador y validez como expresión constante entera. Se detiene justo antes de las funciones.
typeof
Sustituye a una extensión de compilador que ya era obligatoria en la práctica. Aporta introspección de tipos para macros seguras. No aporta higiene de macros ni despacho por tipo.
auto
Sustituye la repetición mecánica de declaraciones. Aporta brevedad donde el tipo es ruido, y cuesta legibilidad donde el tipo es información. Úsalo con criterio.
Igual de instructivo es lo que C decidió no tomar prestado. No hay funciones constexpr, así que la metaprogramación en tiempo de compilación sigue fuera del lenguaje. No hay deducción del tipo de retorno ni auto en parámetros, luego no aparece nada parecido a las plantillas. Y typeof no habilita sobrecarga: para despachar por tipo sigue siendo _Generic la herramienta, con su sintaxis incómoda y su honestidad de que allí no hay magia.
#define raiz(x) _Generic((x), \
float: sqrtf, \
long double: sqrtl, \
default: sqrt)(x)
El criterio de adopción es simple: estas tres construcciones son gratis en ejecución y mejoran la comprobación estática, así que úsalas. Lo único que exige cuidado es no convertir auto en un velo: en C, donde la anchura y el signo de cada tipo determinan la corrección de la aritmética, esconder el tipo es esconder el contrato.
Es tentador leer auto, constexpr y typeof como cosmética tomada de C++, y es una lectura equivocada. Fíjate en qué sustituye cada una: todas reemplazan un mecanismo textual, previo al compilador y ciego a los tipos por un mecanismo que el compilador entiende. Durante décadas, la mitad de la expresividad de C vivía en el preprocesador —constantes, genéricos, tipos deducidos a mano— y el precio era que ni el compilador, ni el depurador, ni el analizador estático, ni tu editor sabían de qué hablabas. Cada macro era una zona ciega en el sistema de tipos. Lo que C23 hace es mover esa expresividad al lado iluminado: constexpr tiene tipo y sale en el depurador; typeof participa en la comprobación de tipos; nullptr es inconfundible en cualquier contexto. El resultado no es un C más abstracto, es un C más analizable, y en un lenguaje donde el compilador es tu única red de seguridad, la analizabilidad es corrección. Nota además lo que el comité rechazó: nada de funciones en compilación, nada de plantillas, nada de coste oculto. C sigue defendiendo su contrato fundacional —lo que escribes es lo que se ejecuta— mientras recupera terreno del preprocesador. Esa es la dirección del lenguaje, y conviene programar alineado con ella.
- Declara con
autouna variable a partir de unconst inty comprueba contypeofque perdió el calificador. - Sustituye tres
#definede constantes porconstexpry usa una como tamaño de array y otra como etiqueta decase. - Intenta usar esa constante en un
#ify explica exactamente por qué el preprocesador la ignora. - Escribe la macro
INTERCAMBIAcontypeof_unqualy pruébala con unconst volatilepara ver la diferencia. - Combina
constexpr autoy comprueba el tipo deducido provocando un error de asignación deliberado.