O que está por trás do ponteiro "gordo"? Análise do fat pointer em Rust
\n\nImagine que você foi a uma biblioteca e pediu um livro. O bibliotecário lhe dá um pequeno papel com o número da estante e da prateleira — isso é um ponteiro comum. Mas e se você não souber o tamanho do livro? Ou se for um livro em três volumes? Você precisará de um segundo papel: "Volume 1 de 3" ou "Comprimento: 450 páginas".
\n\nEm Rust, esse ponteiro "duplo" é chamado de fat pointer (ponteiro gordo). Não é mágica, é apenas um par de números: o endereço dos dados e suas meta-informações (geralmente o comprimento ou um ponteiro para a tabela virtual de métodos). Vamos entender para que serve e como funciona, com exemplos reais de código.
\n\nPonteiros comuns: finos e rápidos
\n\nVamos começar pela base. Em Rust, existem ponteiros simples — &T (referência) ou *const T (ponteiro bruto). Eles ocupam exatamente 8 bytes (em sistemas de 64 bits) e armazenam apenas o endereço na memória.
fn main() {\n let x: i32 = 42;\n let ptr: &i32 = &x; // ponteiro fino — 8 bytes\n println!("Tamanho da referência: {} bytes", std::mem::size_of_val(&ptr));\n}\n\n\nSaída: Tamanho da referência: 8 bytes. Tudo previsível: o compilador sabe que i32 tem 4 bytes, então informações adicionais não são necessárias. Mas e se o tamanho dos dados for desconhecido antecipadamente?
Primeiro ponteiro gordo: slices (&[T])
\n\nUm slice é uma "janela" para um array ou vetor. Mas para usá-lo, você precisa saber não apenas o início, mas também a quantidade de elementos. Rust armazena isso como um par (ptr, len) — isso é um 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!("Tamanho do slice: {} bytes", std::mem::size_of_val(&slice));\n println!("Elementos: {:?}", slice);\n}\n\n\nSaída: Tamanho do slice: 16 bytes (8 bytes ponteiro + 8 bytes comprimento). Se passássemos apenas o endereço, não saberíamos onde parar. É por isso que o slice é um exemplo clássico de fat pointer.
A propósito, a string &str funciona exatamente da mesma forma: é um fat pointer (*const u8, usize). É por isso que &str ocupa 16 bytes, enquanto &String ocupa apenas 8 (porque String armazena o comprimento internamente).
Segundo ponteiro gordo: objetos trait (&dyn Trait)
\n\nEm Rust, existe despacho dinâmico através de objetos trait. Quando você escreve &dyn MyTrait, o compilador não sabe qual tipo concreto será passado. Ele precisa de uma tabela virtual de métodos (vtable) para chamar as funções corretas.
trait Animal {\n fn speak(&self);\n}\n\nstruct Dog;\nstruct Cat;\n\nimpl Animal for Dog {\n fn speak(&self) { println!("Au!"); }\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!("Tamanho de &dyn Animal: {} bytes", std::mem::size_of::<&dyn Animal>());\n\n for animal in &animals {\n animal.speak();\n }\n}\n\n\nSaída: Tamanho de &dyn Animal: 16 bytes. Esses 16 bytes são novamente um fat pointer: os primeiros 8 bytes são o endereço dos dados (Dog ou Cat), os segundos 8 bytes são o endereço da vtable (tabela de métodos).
Como o fat pointer afeta o desempenho?
\n\nPonteiros gordos são um compromisso. Por um lado, eles carregam mais informações, permitindo trabalhar com tipos dinâmicos. Por outro lado:
\n\n- \n
- Memória: um fat pointer ocupa 16 bytes em vez de 8. Isso não é crítico, mas em grandes coleções pode fazer diferença. \n
- Velocidade: o acesso via vtable (para objetos trait) é um pouco mais lento do que a chamada direta de método (monomorfização). A dife