wandres.dev
PRACTICANTE · Tipos y novedades de C23

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.

⏱ 17 min

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.

🎯 Al terminar esta lección sabrás
  • Usar auto para inferir tipos y conocer sus restricciones exactas.
  • Sustituir macros de constantes por constexpr y saber dónde no llega.
  • Escribir macros seguras con typeof y typeof_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
💡
Dónde gana de verdad

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.

⚠️
El límite que sorprende a todo el mundo

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.

C23 no acerca C a otros lenguajes: le devuelve lo que el preprocesador le había robado

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.

⚔️ Moderniza un fichero tuyo
  1. Declara con auto una variable a partir de un const int y comprueba con typeof que perdió el calificador.
  2. Sustituye tres #define de constantes por constexpr y usa una como tamaño de array y otra como etiqueta de case.
  3. Intenta usar esa constante en un #if y explica exactamente por qué el preprocesador la ignora.
  4. Escribe la macro INTERCAMBIA con typeof_unqual y pruébala con un const volatile para ver la diferencia.
  5. Combina constexpr auto y comprueba el tipo deducido provocando un error de asignación deliberado.