Semântica de Movimento

Uma atribuição transferirá o ownership entre variáveis:

fn main() {
    let s1: String = String::from("Olá!");
    let s2: String = s1;
    println!("s2: {s2}");
    // println!("s1: {s1}");
}
  • A atribuição de s1 a s2 transfere o ownership.
  • Quando s1 sai do escopo, nada acontece: ele não tem ownership.
  • Quando s2 sai do escopo, os dados da string são liberados.

Antes de mover para s2:

StackHeaps1ptrHello!len6capacity6

Depois de mover para s2:

StackHeaps1ptrHello!tam6capacid6s2ptrtam6capacid6(inacessível)

Quando você passa um valor para uma função, o valor é atribuído ao parâmetro da função. Isso transfere a ownership:

fn say_hello(name: String) {
    println!("Olá {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(name);
    // say_hello(name);
}
This slide should take about 5 minutes.
  • Mencione que isso é o oposto dos defaults (padrões) em C++, que copia por valor, a menos que você use std::move (e seu construtor esteja definido!).

  • Apenas o ownership é movido. A geração de código de máquina para manipular os dados é uma questão de otimização, e essas cópias são agressivamente otimizadas.

  • Valores simples (tais como inteiros) podem ser marcados como Copy (cópia) (veja slides mais adiante).

  • No Rust, clones são explícitos (utilizando-se clone).

No exemplo say_hello:

  • Com a primeira chamada para diga_ola, main desiste da ownership de nome. Depois disso, nome não pode mais ser usado dentro de main.
  • A memória do heap alocada para name será liberada no final da função say_hello.
  • main pode manter a ownership se passar nome como uma referência (&name) e se say_hello aceitar uma referência como um parâmetro.
  • Alternativamente, main pode passar um clone de nome na primeira chamada (name.clone()).
  • Rust torna mais difícil a criação de cópias inadvertidamente do que o C++, tornando padrão a semântica de movimento e forçando os programadores a tornar os clones explícitos.

Mais para Explorar

Cópias Defensivas em C++ Moderno

O C++ moderno resolve isso de maneira diferente:

std::string s1 = "Cpp";
std::string s2 = s1;  // Duplica os dados em s1.
  • Os dados de s1 no heap são duplicados e s2 obtém sua própria cópia independente.
  • Quando s1 e s2 saem de escopo, cada um libera sua própria memória.

Antes da atribuição por cópia:

StackHeaps1ptrCpplen3capacity3

Após atribuição por cópia:

StackHeaps1ptrCpplen3capacity3s2ptrCpplen3capacity3

Pontos chave:

  • O C++ fez uma escolha ligeiramente diferente do Rust. Como = copia dados, os dados da string devem ser clonados. Caso contrário, obteríamos uma dupla liberação quando qualquer string saísse de escopo.

  • O C++ também possui std::move, que é usado para indicar quando um valor pode ser movido. Se o exemplo fosse s2 = std::move(s1), nenhuma alocação de heap seria feita. Após a movimentação, s1 estaria em um estado válido, mas não especificado. Diferentemente do Rust, o programador pode continuar usando s1.

  • Diferentemente do Rust, = em C++ pode executar código arbitrário conforme determinado pelo tipo que está sendo copiado ou movido.