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 vs Rc/Arc:
- 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 vs RefCell:
- 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 vs referências (&T):
- 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