Ponteiros gordos em Rust: entendendo o fat pointer

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

O que está por trás do ponteiro "gordo"? Análise do fat pointer em Rust

\n\n

Imagine 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\n

Em 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\n

Ponteiros comuns: finos e rápidos

\n\n

Vamos 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.

\n\n
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\n

Saí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?

\n\n

Primeiro ponteiro gordo: slices (&[T])

\n\n

Um 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.

\n\n
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\n

Saí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.

\n\n

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).

\n\n

Segundo ponteiro gordo: objetos trait (&dyn Trait)

\n\n

Em 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.

\n\n
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\n

Saí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).

\n\n

Como o fat pointer afeta o desempenho?

\n\n

Ponteiros 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

Blogs

Book Recommendations