Box: alocação no heap
1. Introdução ao Box e alocação no heap
Em Rust, a memória é gerenciada de forma previsível e segura através de um sistema de ownership. A stack (pilha) é usada para dados de tamanho conhecido em tempo de compilação, enquanto o heap (monte) é reservado para dados dinâmicos. O Box<T> é um ponteiro inteligente que permite alocar dados no heap, mantendo um ponteiro na stack que aponta para o valor armazenado.
A principal diferença entre stack e heap está no gerenciamento de memória: na stack, os dados são alocados e liberados automaticamente seguindo a ordem LIFO (Last In, First Out). No heap, a alocação é mais flexível, mas requer gerenciamento explícito — algo que Rust faz automaticamente através do Box<T>.
Quando usar Box
- Tipos de tamanho dinâmico (como trait objects)
- Estruturas de dados recursivas (listas encadeadas, árvores)
- Quando você precisa de ownership sobre um valor grande que não deve ser copiado
2. Criando e usando Box
A sintaxe básica para criar um Box<T> é Box::new(valor). O valor é alocado no heap, e um ponteiro para ele é retornado.
fn main() {
// Alocando um inteiro no heap
let valor_heap: Box<i32> = Box::new(42);
// Acessando o valor através de desreferenciação
println!("Valor no heap: {}", *valor_heap);
// Rust também faz auto-desreferenciação em muitos casos
println!("Valor no heap (automático): {}", valor_heap);
// Modificando o valor (requer mutabilidade)
let mut valor_mutavel: Box<i32> = Box::new(10);
*valor_mutavel += 5;
println!("Valor modificado: {}", valor_mutavel);
}
A desreferenciação com * permite acessar o valor contido no Box, mas Rust frequentemente aplica auto-desreferenciação em chamadas de métodos e operadores, tornando o código mais limpo.
3. Box para tipos recursivos
Um dos usos mais importantes do Box<T> é resolver o problema de tipos recursivos. Considere uma lista encadeada simples (cons-list):
// Esta definição NÃO compila: tamanho infinito
// enum List {
// Cons(i32, List),
// Nil,
// }
// Solução com Box: tamanho conhecido (ponteiro)
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
// Criando uma lista: 1 -> 2 -> 3 -> Nil
let lista = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("Lista: {:?}", lista);
// Percorrendo a lista
let mut atual = &lista;
loop {
match atual {
List::Cons(valor, proximo) => {
println!("Valor: {}", valor);
atual = proximo;
}
List::Nil => break,
}
}
}
Sem o Box, o compilador não conseguiria determinar o tamanho da enumeração List, pois ela conteria a si mesma recursivamente. Com Box, temos um ponteiro de tamanho fixo (usualmente 8 ou 16 bytes), resolvendo o problema.
4. Box com trait objects
Box<dyn Trait> permite polimorfismo em tempo de execução, armazenando diferentes tipos que implementam a mesma trait:
trait Animal {
fn fazer_som(&self);
}
struct Cachorro;
struct Gato;
impl Animal for Cachorro {
fn fazer_som(&self) {
println!("Au au!");
}
}
impl Animal for Gato {
fn fazer_som(&self) {
println!("Miau!");
}
}
fn main() {
// Vetor de trait objects: diferentes tipos no mesmo vetor
let animais: Vec<Box<dyn Animal>> = vec![
Box::new(Cachorro),
Box::new(Gato),
Box::new(Cachorro),
];
for animal in animais {
animal.fazer_som(); // Dispatch dinâmico
}
}
Comparação com genéricos: Genéricos usam monomorfização (código duplicado para cada tipo), enquanto Box<dyn Trait> usa dispatch dinâmico (vtable). Genéricos são mais rápidos, mas Box<dyn Trait> oferece mais flexibilidade quando os tipos são conhecidos apenas em tempo de execução.
5. Box e ownership
O Box<T> é o único proprietário do dado no heap. Quando o Box é movido, a ownership do dado também é transferida:
fn main() {
let box1 = Box::new(String::from("Hello"));
let box2 = box1; // Ownership transferida para box2
// println!("{}", box1); // ERRO: box1 foi movido
println!("{}", box2); // OK: box2 é o proprietário
// Liberação automática quando o Box sai de escopo
{
let box_temporario = Box::new(100);
println!("Temporário: {}", box_temporario);
} // box_temporario é dropado aqui, memória liberada
println!("Fim do escopo principal");
}
Quando um Box sai de escopo, o destructor Drop é chamado automaticamente, liberando a memória no heap. Isso garante que não haja vazamentos de memória.
6. Box em estruturas de dados
Box<T> é essencial para implementar estruturas de dados complexas:
#[derive(Debug)]
struct No {
valor: i32,
esquerda: Option<Box<No>>,
direita: Option<Box<No>>,
}
impl No {
fn novo(valor: i32) -> Self {
No {
valor,
esquerda: None,
direita: None,
}
}
}
fn main() {
// Construindo uma árvore binária simples
let raiz = No {
valor: 10,
esquerda: Some(Box::new(No {
valor: 5,
esquerda: None,
direita: None,
})),
direita: Some(Box::new(No {
valor: 15,
esquerda: None,
direita: None,
})),
};
println!("Raiz: {:?}", raiz);
// Box<[T]>: array com tamanho dinâmico no heap
let array_heap: Box<[i32]> = vec![1, 2, 3, 4, 5].into_boxed_slice();
println!("Array no heap: {:?}", array_heap);
println!("Tamanho: {}", array_heap.len());
}
Box<[T]> é útil quando você precisa de um array com tamanho conhecido apenas em tempo de execução, mas não precisa de realocação dinâmica (como Vec).
7. Performance e considerações
Custos de alocação: Alocar no heap é mais caro que na stack, pois envolve busca por espaço livre e gerenciamento de memória. Para dados pequenos, a stack é sempre preferível.
Box como ponteiro fino: Box<T> é um ponteiro fino (thin pointer) — ele tem exatamente o tamanho de um ponteiro nativo (8 bytes em sistemas 64-bit). Não há overhead extra além da alocação no heap.
Padrões de uso recomendados:
- Use Box quando precisar de tipos recursivos ou trait objects
- Evite Box para tipos pequenos que poderiam ficar na stack
- Prefira Vec para coleções dinâmicas (a menos que precise de Box<[T]>)
fn main() {
// Evite: Box para tipos pequenos
let ruim = Box::new(42); // Desnecessário: i32 cabe na stack
// Prefira: valor direto na stack
let bom = 42;
// Box é útil para dados grandes ou de tamanho desconhecido
let dados_grandes = Box::new([0u8; 1024 * 1024]); // 1MB no heap
println!("Dados alocados: {} bytes", std::mem::size_of_val(&*dados_grandes));
}
8. Comparação com outros ponteiros inteligentes
Box
- Box<T>: ownership único, sem contagem de referências
- Rc<T>/Arc<T>: ownership compartilhado com contagem de referências
- Use Box quando apenas um proprietário é necessário
use std::rc::Rc;
fn main() {
let unico = Box::new(42); // Um dono
let compartilhado = Rc::new(42); // Múltiplos donos possíveis
let clone1 = Rc::clone(&compartilhado);
let clone2 = Rc::clone(&compartilhado);
println!("Contagem de referências: {}", Rc::strong_count(&compartilhado));
}
Box
- Box<T>: mutabilidade padrão (precisa de let mut)
- RefCell<T>: mutabilidade interior em tempo de execução
- Box não oferece mutabilidade interior
Box
- Box<T>: ownership sobre o dado, vive enquanto existir
- &T: empréstimo (borrow), não é proprietário
- Use Box quando precisar de ownership; use &T para acesso temporário
fn processar(valor: &i32) {
println!("Processando: {}", valor);
}
fn main() {
let dado = Box::new(100);
processar(&dado); // Emprestando o valor do Box
// Após processar, dado ainda é válido
println!("Ainda dono: {}", dado);
}
Referências
- The Rust Programming Language - Chapter 15.1: Using Box
to Point to Data on the Heap — Capítulo oficial do livro de Rust sobre Box, com exemplos detalhados de alocação no heap e tipos recursivos. - Rust by Example - Box, stack and heap — Tutoriais práticos mostrando o uso de Box
em diferentes contextos, incluindo alocação e desreferenciação. - Rust Reference - Type Layout: Box
— Documentação técnica sobre o layout de memória do Box e como ele interage com o sistema de tipos. - Rustnomicon - Box
— Guia avançado sobre Box , incluindo implementação manual e considerações de unsafe code. - Learn Rust With Entirely Too Many Linked Lists — Tutorial clássico sobre implementação de listas encadeadas em Rust, usando Box
extensivamente para tipos recursivos. - The Rust Performance Book - Box
and Heap Allocation — Guia de performance focado em alocação no heap, com dicas sobre quando usar Boxe quando evitar.