wandres.dev
ESPECIALISTA · Enums y typedef

typedef: cuándo ayuda y cuándo esconde

typedef no crea tipos, crea nombres. Dónde ese nombre es imprescindible, por qué esconder un puntero tras un alias envenena el const y la propiedad, y qué dice el estilo del kernel de Linux sobre todo esto.

⏱ 17 min

typedef es la construcción de C que más discusiones ha provocado por metro cuadrado de gramática. Unos la usan en todas partes y otros la prohíben casi entera, y ambos bandos tienen razón en la mitad de sus argumentos. La forma de salir del empate no es elegir bando: es entender qué hace exactamente typedef —un alias léxico, nunca un tipo nuevo— y deducir de ahí en qué casos ese alias añade información y en cuáles la destruye.

🎯 Al terminar esta lección sabrás
  • Interiorizar que typedef no introduce un tipo nuevo, solo un sinónimo, y qué implica eso.
  • Reconocer los casos donde el alias es imprescindible: punteros a función, arrays y portabilidad.
  • Entender por qué esconder un puntero tras un alias envenena const, la propiedad y la nulabilidad.
  • Situar la regla del kernel de Linux y construir con ella un criterio propio defendible.

typedef no crea un tipo: crea un nombre

La norma es explícita hasta la sequedad: una declaración typedef no introduce un tipo nuevo, solo un sinónimo del tipo especificado. De ahí se sigue todo lo demás. Dos alias del mismo tipo son intercambiables sin conversión, sin aviso y sin diagnóstico; el compilador no distingue jamás entre el alias y el tipo aliasado.

typedef int Metros;
typedef int Segundos;

Metros   d = 100;
Segundos t = 5;
d = t;              /* compila en silencio: ambos son 'int' */

Si esperabas que typedef te diera seguridad de tipos, este ejemplo es la respuesta: no la da, y ninguna cantidad de disciplina en el nombre la va a dar. La distinción real exige envolver el valor en una struct, que es un tipo genuinamente nuevo, y eso es materia de la última lección de este nivel.

Gramaticalmente, typedef es un especificador de clase de almacenamiento, igual que static o extern. Esa peculiaridad explica su sintaxis desconcertante: el nombre del alias ocupa exactamente la posición donde iría el nombre de la variable si borrases la palabra typedef.

int (*op)(int, int);              /* 'op' es un puntero a funcion */
typedef int (*Operacion)(int, int);  /* 'Operacion' nombra ese tipo */

char *cadenas[10];                /* 'cadenas' es un array de 10 punteros */
typedef char *Tabla[10];          /* 'Tabla' nombra ese tipo */

Leer un typedef es, literalmente, leer una declaración de variable y sustituir mentalmente la variable por un nombre de tipo. Quien domina esa regla deja de necesitar herramientas externas para descifrar declaraciones enrevesadas.

Hay un detalle que explica el idioma más común de todos. Las etiquetas de struct, union y enum viven en un espacio de nombres propio, separado del de los identificadores ordinarios, así que la etiqueta y el alias pueden llamarse igual sin colisionar:

typedef struct Punto Punto;   /* etiqueta 'Punto' y alias 'Punto': no chocan */
struct Punto { int x, y; };

Esa línea, que a primera vista parece una definición circular, es solo la convención para no tener que escribir la palabra struct en cada uso. Y es también el punto exacto donde arranca la discusión: si escribirla o no es precisamente lo que está en disputa.

ℹ️
El sufijo _t está reservado

POSIX reserva para futuras versiones del estándar todos los identificadores terminados en _t en el espacio de nombres global. Llamar conexion_t a tu alias es legal hoy y puede colisionar mañana con un nombre de la implementación, sin que el compilador te avise de nada más que un error críptico de redefinición. La convención segura en código de aplicación es un sufijo propio o, más simple, capitalización: Conexion. Que el estándar y glibc usen _t por todas partes no es permiso, es exactamente lo contrario: es la razón por la que está reservado.

Dónde el alias es imprescindible

Hay tres familias de casos en los que quitar el typedef empeora el código de forma objetiva, no estética.

La primera son los punteros a función. Su declarador es tan hostil que sin alias el código deja de ser mantenible, y con alias se lee como una firma.

typedef int (*Comparador)(const void *, const void *);

void ordenar(void *base, size_t n, size_t tam, Comparador cmp);

/* Sin el alias, la misma firma seria: */
void ordenar_sin(void *base, size_t n, size_t tam,
                 int (*cmp)(const void *, const void *));

La segunda es la portabilidad de la representación. size_t, uint32_t, time_t, ptrdiff_t existen porque su representación concreta es precisamente lo que no debes conocer. Aquí el alias no oculta información útil: oculta información que si conocieras te haría escribir código no portable. Es abstracción legítima.

La tercera son los tipos opacos, donde el alias acompaña a una declaración de struct incompleta y la ocultación es el objetivo declarado de la interfaz. FILE es el ejemplo canónico: llevas años usándolo sin haber visto jamás sus campos, y esa ignorancia es la que permite a cada implementación de la biblioteca estándar hacer lo que quiera por dentro.

💡
C23 reduce algunas necesidades de alias

C23 incorpora typeof y typeof_unqual, que permiten nombrar el tipo de una expresión sin inventarle un nombre. En macros genéricas eso elimina buena parte de los typedef auxiliares que antes había que crear solo para poder declarar una variable temporal del tipo correcto. También llega auto como deducción de tipo en definiciones de objeto. Ninguna de las dos sustituye a typedef en las interfaces públicas —un header sigue necesitando nombres estables—, pero ambas retiran del typedef una carga que nunca le correspondió: la de ser el único mecanismo del lenguaje para hablar de tipos sin escribirlos.

El puntero escondido: el caso que casi siempre está mal

El uso discutido es este:

typedef struct Conexion *ConexionPtr;   /* el alias se traga el asterisco */

Parece inocuo y no lo es, por tres motivos independientes que se acumulan.

El primero y más grave es que const deja de significar lo que crees. Un calificador aplicado a un alias califica al tipo completo del alias, no a lo que hay al otro lado de la indirección:

typedef char *Cadena;

const Cadena a = "hola";   /* esto es 'char * const a': puntero constante
                              a caracteres MUTABLES */
const char  *b = "hola";   /* esto si es lo que casi siempre querias */

a[0] = 'H';    /* compila: los caracteres no son const */

Nadie razona bien sobre const cuando el asterisco está escondido. El segundo motivo es que el alias borra la indirección, que es exactamente el dato que el lector necesita para razonar sobre propiedad y sobre nulabilidad: si veo ConexionPtr c, no sé si c puede ser nulo, si soy responsable de liberarla, ni si pasarla a otra función transfiere la posesión. El asterisco no responde solas esas preguntas, pero es la señal que hace que el lector se las plantee.

El tercero es la aritmética silenciosa: ConexionPtr p; p += 1; compila sin protestar y avanza sizeof(struct Conexion) bytes, algo que parecería absurdo escrito con el asterisco visible.

El mismo defecto reaparece, aún más disimulado, con los arrays:

typedef int Vector[10];

void f(Vector v);      /* NO recibe un array: el parametro decae a 'int *' */
const Vector cv;       /* array de 10 'const int', no un array constante */
Vector g(void);        /* ERROR: una funcion no puede devolver un array */

Nada de eso es sorprendente cuando el corchete está a la vista; con el alias, un lector razonable supone que Vector se comporta como un tipo cualquiera, y las tres líneas lo desmienten de tres maneras distintas. La regla general que se destila de estos casos es que typedef es seguro cuando aliasa un tipo completo y sin declarador, y sospechoso en cuanto se traga un *, un [ ] o un paréntesis de función que el usuario todavía necesita ver.

La excepción sensata es el tipo opaco: ahí el alias esconde el puntero a propósito, porque el usuario no debe saber siquiera que hay una indirección, y la API completa está diseñada para que nunca necesite saberlo. La diferencia entre las dos situaciones no es sintáctica: es si existe o no un contrato que sustituya a la información que has borrado.

La regla del kernel y la síntesis

El documento de estilo del kernel de Linux dedica un capítulo entero al asunto y su posición es célebre por lo tajante: no uses typedef para structs ni para punteros, porque el lector de una función debe poder ver de un vistazo qué es cada cosa. struct trabajo *t dice qué es t; trabajo_t t no dice nada.

El mismo documento enumera las excepciones que sí acepta, y son precisamente las tres familias que hemos aislado: objetos totalmente opacos, donde el alias es la interfaz y el tipo real es inaccesible por diseño —atomic_t, spinlock_t, pte_t—; tipos enteros con nombre claro que sirven a la portabilidad —u8, u32, __u32 para lo que cruza a espacio de usuario—; y las situaciones donde el alias existe para poder cambiar la representación sin tocar a los usuarios. Nada más.

Conviene reconocer el contraejemplo antes de que alguien lo esgrima: la biblioteca estándar y POSIX están llenas de typedefsize_t, time_t, pthread_t, FILE— y nadie los considera un error. La contradicción es aparente. Todos ellos son casos de la segunda o la tercera familia: su representación concreta es exactamente lo que la interfaz promete no revelar, y por eso el alias no borra información, la protege. El kernel no prohíbe typedef, prohíbe el typedef decorativo: el que se pone sobre una struct que el lector sí puede y debe abrir.

De todo lo anterior se destila una prueba de tres preguntas para cualquier alias que estés a punto de escribir. ¿El usuario necesita conocer la representación para usar bien la API? Si la respuesta es sí, el alias sobra. ¿El alias se traga un *, un [ ] o un declarador de función? Si es sí y no hay opacidad detrás, es un alias tóxico. ¿Existe una imposibilidad que respalde el nombre, sea un tipo incompleto, una anchura garantizada o una API cerrada? Si la hay, el alias es abstracción legítima; si no la hay, es solo escribir menos.

Alias justificado

Punteros a función, tipos opacos e independencia de la representación. En los tres, la información oculta es información que el usuario no debe usar.

Alias que estorba

Structs corrientes y punteros normales. Ahí el alias borra struct y el asterisco, que son justo lo que el lector necesita para razonar.

Un nombre no es una abstracción

El error de fondo del debate sobre typedef es confundir dar un nombre con crear una frontera. Un alias es una operación puramente léxica: sustituye texto y desaparece; el compilador no lo recuerda, no lo comprueba y no lo hace cumplir. Lo que de verdad crea una frontera en C es la incompletitud: un struct declarado sin cuerpo en el header es un tipo cuyo tamaño y cuyos campos el compilador se niega a conocer, y esa negativa se convierte en un error si alguien intenta violarla. Ahí hay un contrato, porque hay algo que el lenguaje impide hacer. Un typedef sobre un tipo completo no impide nada: cualquiera puede seguir tocando los campos, hacer aritmética con el puntero y sustituir el alias por su definición sin cambiar el significado del programa. De ahí sale el criterio que resuelve el empate entre bandos: pregunta si el nombre viene acompañado de una imposibilidad. Si el alias oculta algo que el usuario no puede usar aunque quiera —un FILE, un pthread_mutex_t, un uint32_t cuya anchura es el contrato entero—, estás abstrayendo. Si oculta algo que el usuario sigue pudiendo usar y de hecho necesita para razonar —un asterisco, la palabra struct—, no estás abstrayendo: estás escribiendo menos a costa de que el siguiente lector sepa menos. Toda la disciplina de tipos en C se reduce a distinguir esas dos cosas.

⚔️ Somete tus alias a juicio
  1. Declara typedef char *Cadena y comprueba con el compilador qué significa const Cadena c frente a const char *c. Explica la diferencia en una frase.
  2. Reescribe una firma con puntero a función anidado con y sin typedef y decide cuál dejarías en un header público.
  3. Recorre un archivo tuyo y clasifica cada typedef en una de las tres familias justificadas o en la papelera. Elimina los de la papelera y mide si el código empeoró.
  4. Explica por qué atomic_t del kernel es un typedef aceptable mientras trabajo_t sobre una struct pública no lo es.
  5. Toma una macro genérica tuya que necesitaba un alias auxiliar y reescríbela con typeof de C23. Valora si el alias sobraba.