Ponteiros inteligentes em Rust
(smart pointers)
(parte 1)
Clube do Livro #15
O que são ponteiros?
- Um ponteiro é uma variável que contém um endereço de memória onde um dado está. Ele aponta para o dado.
- Em rust, o ponteiro mais comum é a referência (&) (Capítulo 4)
Clube do Livro #15

O que são ponteiros inteligentes?
-
Smart Pointers não somente apontam, mas podem ter:
- metadados
- comportamento customizado
- na maioria dos casos, são os donos do dado (especificamente em Rust)
- Exemplo: String
- é dona do dado
- metadados: tamanho, capacidade
- comportamento: permite apenas UTF-8 válido
-
Smart Pointers geralmente são implementados usando structs
- Quase sempre implementam as traits Deref e Drop
Clube do Livro #15
O que são ponteiros inteligentes?
- Smart pointers são um design pattern em Rust
- A biblioteca-padrão provê vários tipos de ponteiros inteligentes, os mais comuns são:
- Box<T>, para alocar valores na heap
- Rc<T>, um contador de referências que permite múltiplos donos de um mesmo valor
- Ref<T> e RefMut<T>, acessados por RefCell<T>, para utilizar as regras do borrow checker em tempo de execução ao invés de tempo de compilação, para casos especiais
Clube do Livro #15
O smart pointer Box<T>
- Box permite guardar valores na heap
- O que fica na stack é apenas um ponteiro para os dados na heap
- Situações de uso:
- Um tipo cujo tamanho não pode ser conhecido no momento da compilação e deseja usar um valor desse tipo em um contexto que requer um tamanho exato
-
Uma grande quantidade de dados e deseja transferir a propriedade, sem ter que copiá-los
-
Quando você deseja alterar a propriedade um valor e se preocupa apenas se ele implementa um trait específica ao invés de ser de um tipo específico
Clube do Livro #15
Clube do Livro #15

fn main() {
let b = Box::new(5);
println!("b = {}", b);
}Um caso recursivo para Box<T>
- Uma estrutura recursiva refere-se a si mesma
- Rust precisa saber, em tempo de compilação, quanto um tipo ocupará na memória
- Um tipo recursivo não permite saber o seu tamanho, pois a sua própria definição é recursiva, o que confunde o compilador
- Como Box<T> tem um tamanho fixo, envolveremos os nossos dados nele, que os salvará na heap, e, ao mesmo tempo, terá um tamanho fixo, o que agradará o compilador
- Vamos explicar isso com um exemplo: cons list
Clube do Livro #15
Cons list
- Estrutura de dados comum em linguagens funcionais, como o LISP
- Estrutura consite num par de valores: um guarda o valor atual e o outro aponta para o próximo valor. Estes pares formam uma lista. O último item contém apenas um valor, NIL.
Clube do Livro #15

Clube do Livro #15
fn main() {
let list = Cons(42, Cons(69, Cons(613, Nil)));
}

Clube do Livro #15
Resolvendo o problema
- Já que Box<T> tem um tamanho fixo, podemos usar ela para resolver esse problema e implementar a nossa estrutura recursiva.
- O tamanho de um ponteiro não está definido em função do tamanho dos dados para os quais ele aponta
-
Isso significa que podemos colocar um Box <T> dentro da variante Cons em vez de outro valor List diretamente. O Box <T> apontará para o próximo valor de List que estará na heap, e não dentro da variante Cons.
-
Dessa forma o compilador determinará que a variante Cons precisará do tamanho de um i32 mais o espaço para armazenar os dados do ponteiro Box<List>.
Clube do Livro #15
Um sp e a trait Deref
- Vamos implementar um sp
- O nosso ponteiro irá implementar as traits Deref e Drop
- De-refereciamento: pegar o valor que um ponteiro aponta
-
Para usar o "de-referenciamento" com nosso sp temos que implementar a trait Deref.
Clube do Livro #15
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}Clube do Livro #15
Coerção de de-referências
- Funcionalidade de conveniência de Rust que opera sobre argumentos para funções e métodos ao converter um tipo de uma referência para outro tipo, aceito pela função
-
A coerção acontece automaticamente quando passamos uma referência para um tipo particular como argumento numa função ou método, sendo que o esse tipo não bate com a definição do argumento da função ou método.
-
Rust executa uma sequência de chamadas para o método deref para converter o tipo passado para o tipo que a função ou método precisa.
Clube do Livro #15
Exemplo de coereção
- Estamos tentando chamar hello com o nosso tipo, mas hello espera um tipo &str
- Como implementamos deref no nosso tipo, o compilador chamará essa função, que fará a conversão para String
- String, por sua vez, possui uma implementação de Deref para converter seu valor para um slice de string (&str), Rust irá chamar o método deref dessa implementação
- Nossa função receberá o slice de string necessário
- Se Rust não tivesse coerção, teríamos que escrever:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}Clube do Livro #15
Limpeza com a trait Drop
- Trait drop: permite definir o que acontece quando um valor está para sair de escopo - permite definir a nossa própria limpeza de recursos automática
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
...
}
}Clube do Livro #15
Limpeza prematura com drop()
- Existem situações em que precisamos liberar um recurso antes da liberação automática feita pelo Rust
- Neste caso usamos std::mem::drop, que está no prelúdio, portanto apenas drop():
fn main() {
let c = CustomSmartPointer {
data: String::from("dados"),
};
println!("CustomSmartPointer criado.");
drop(c);
println!("CustomSmartPointer liberado antes.");
}Obrigado, pessoal!

@felubra
twitter, github, telegram:
Referências
Clube do Livro #15
Ponteiros inteligentes em Rust (parte 1)
By Felipe Lube de Bragança
Ponteiros inteligentes em Rust (parte 1)
Material de apoio para apresentação do capítulo 15 do Livro do Rust em nosso canal do youtube, dia 17/01/2021 às 21h.
- 170