¿Qué se esconde detrás del puntero «gordo»? Análisis del fat pointer en Rust
\n\nImagina que vas a una biblioteca y pides un libro. El bibliotecario te da un pequeño papel con el número de estantería y el estante: esto es un puntero normal. Pero, ¿y si no sabes el tamaño del libro? ¿O si resulta que es una trilogía? Necesitarás un segundo papel: «Tomo 1 de 3» o «Longitud: 450 páginas».
\n\nEn Rust, este puntero «doble» se llama fat pointer (puntero gordo). No es magia, sino simplemente un par de números: la dirección de los datos y su metainformación (generalmente la longitud o un puntero a la tabla virtual de métodos). Analicemos para qué sirve y cómo funciona, con ejemplos de código reales.
\n\nPunteros normales: delgados y rápidos
\n\nEmpecemos por lo básico. En Rust hay punteros simples: &T (referencia) o *const T (puntero crudo). Ocupan exactamente 8 bytes (en un sistema de 64 bits) y solo almacenan una dirección en memoria.
fn main() {\n let x: i32 = 42;\n let ptr: &i32 = &x; // puntero delgado — 8 bytes\n println!("Tamaño de la referencia: {} bytes", std::mem::size_of_val(&ptr));\n}\n\n\nSalida: Tamaño de la referencia: 8 bytes. Todo predecible: el compilador sabe que i32 son 4 bytes, por lo que no necesita información adicional. Pero, ¿qué pasa si el tamaño de los datos no se conoce de antemano?
Primer puntero gordo: slices (&[T])
\n\nUn slice es una «ventana» a un array o vector. Pero para usarlo, necesitas saber no solo el inicio, sino también la cantidad de elementos. Rust almacena esto como un par (ptr, len) — esto es un fat pointer.
fn main() {\n let arr = [1, 2, 3, 4, 5];\n let slice: &[i32] = &arr[1..4]; // slice de 3 elementos\n\n println!("Tamaño del slice: {} bytes", std::mem::size_of_val(&slice));\n println!("Elementos: {:?}", slice);\n}\n\n\nSalida: Tamaño del slice: 16 bytes (8 bytes del puntero + 8 bytes de la longitud). Si solo pasáramos la dirección, no sabríamos dónde detenernos. Por eso el slice es un ejemplo clásico de fat pointer.
Por cierto, la cadena &str funciona exactamente igual: es un fat pointer (*const u8, usize). Es por eso que &str ocupa 16 bytes, mientras que &String solo ocupa 8 (porque String almacena la longitud internamente).
Segundo puntero gordo: objetos trait (&dyn Trait)
\n\nEn Rust existe la despachación dinámica a través de objetos trait. Cuando escribes &dyn MyTrait, el compilador no sabe qué tipo concreto se pasará. Necesita una tabla virtual de métodos (vtable) para llamar a las funciones correctas.
trait Animal {\n fn speak(&self);\n}\n\nstruct Dog;\nstruct Cat;\n\nimpl Animal for Dog {\n fn speak(&self) { println!("¡Guau!"); }\n}\n\nimpl Animal for Cat {\n fn speak(&self) { println!("¡Miau!"); }\n}\n\nfn main() {\n let dog = Dog;\n let cat = Cat;\n\n let animals: [&dyn Animal; 2] = [&dog, &cat];\n\n println!("Tamaño de &dyn Animal: {} bytes", std::mem::size_of::<&dyn Animal>());\n\n for animal in &animals {\n animal.speak();\n }\n}\n\n\nSalida: Tamaño de &dyn Animal: 16 bytes. Estos 16 bytes son nuevamente un fat pointer: los primeros 8 bytes son la dirección de los datos (Dog o Cat), los segundos 8 bytes son la dirección de la vtable (tabla de métodos).
¿Cómo afecta el fat pointer al rendimiento?
\n\nLos fat pointers son un compromiso. Por un lado, llevan más información, lo que permite trabajar con tipos dinámicos. Por otro lado:
\n\n- \n
- Memoria: un fat pointer ocupa 16 bytes en lugar de 8. No es crítico, pero en colecciones grandes puede notarse. \n
- Velocidad: el acceso a través de la vtable (para objetos trait) es un poco más lento que la llamada directa a un método (monomorfización). La dife