É possível tornar um tipo apenas móvel e não copiável?

96

Nota do Editor : esta pergunta foi feita antes do Rust 1.0 e algumas das afirmações na pergunta não são necessariamente verdadeiras no Rust 1.0. Algumas respostas foram atualizadas para atender às duas versões.

Eu tenho esta estrutura

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
}

Se eu passar isso para uma função, ele será copiado implicitamente. Agora, às vezes eu leio que alguns valores não são copiáveis ​​e, portanto, precisam ser movidos.

Seria possível tornar esta estrutura Tripletnão copiável? Por exemplo, seria possível implementar um traço que tornasse Tripletnão copiável e, portanto, "móvel"?

Eu li em algum lugar que é preciso implementar a Clonecaracterística de copiar coisas que não são copiáveis ​​implicitamente, mas nunca li sobre o contrário, que é ter algo que é implicitamente copiável e torná-lo não copiável para que seja movido em seu lugar.

Isso faz algum sentido?

Christoph
fonte
1
paulkoerbitz.de/posts/… . Boas explicações aqui de por que mover versus copiar.
Sean Perry

Respostas:

164

Prefácio : Esta resposta foi escrita antes que as características integradas opt-in - especificamente os Copyaspectos - fossem implementadas. Usei aspas em bloco para indicar as seções que se aplicavam apenas ao esquema antigo (aquele que se aplicava quando a pergunta foi feita).


Antigo : para responder à pergunta básica, você pode adicionar um campo de marcador armazenando um NoCopyvalor . Por exemplo

struct Triplet {
    one: int,
    two: int,
    three: int,
    _marker: NoCopy
}

Você também pode fazer isso tendo um destruidor (por meio da implementação da Dropcaracterística ), mas usar os tipos de marcador é preferível se o destruidor não estiver fazendo nada.

Os tipos agora se movem por padrão, ou seja, quando você define um novo tipo, ele não implementa, a Copymenos que você o implemente explicitamente para o seu tipo:

struct Triplet {
    one: i32,
    two: i32,
    three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move

A implementação só pode existir se cada tipo contido no novo structou enumfor ele mesmo Copy. Caso contrário, o compilador imprimirá uma mensagem de erro. Ele também só pode existir se o tipo não tiver uma Dropimplementação.


Para responder à pergunta que você não fez ... "o que há com moves e copy?":

Em primeiro lugar, definirei duas "cópias" diferentes:

  • uma cópia de byte , que está apenas copiando superficialmente um objeto byte a byte, não seguindo ponteiros, por exemplo, se você tiver (&usize, u64), é de 16 bytes em um computador de 64 bits, e uma cópia superficial tomaria esses 16 bytes e replicaria seus valor em algum outro pedaço de memória de 16 bytes, sem tocar usizeno na outra extremidade do &. Ou seja, é equivalente a chamarmemcpy .
  • uma cópia semântica , duplicando um valor para criar uma nova instância (um tanto) independente que pode ser usada com segurança separadamente da antiga. Por exemplo, uma cópia semântica de um Rc<T>envolve apenas o aumento da contagem de referência, e uma cópia semântica de um Vec<T>envolve a criação de uma nova alocação e, em seguida, a cópia semântica de cada elemento armazenado do antigo para o novo. Podem ser cópias profundas (por exemplo Vec<T>) ou rasas (por exemplo Rc<T>, não toca no armazenado T), Cloneé definido vagamente como a menor quantidade de trabalho necessária para copiar semanticamente um valor do tipo Tde dentro de &Tpara T.

Rust é como C, cada uso por valor de um valor é uma cópia de byte:

let x: T = ...;
let y: T = x; // byte copy

fn foo(z: T) -> T {
    return z // byte copy
}

foo(y) // byte copy

Eles são cópias de bytes, sejam ou não T movidos ou ou "implicitamente copiáveis". (Para ser claro, eles não são necessariamente cópias literalmente byte a byte em tempo de execução: o compilador é livre para otimizar as cópias se o comportamento do código for preservado.)

No entanto, há um problema fundamental com cópias de byte: você acaba com valores duplicados na memória, o que pode ser muito ruim se eles tiverem destruidores, por exemplo

{
    let v: Vec<u8> = vec![1, 2, 3];
    let w: Vec<u8> = v;
} // destructors run here

Se wfosse apenas uma cópia simples de ventão haveria dois vetores apontando para a mesma alocação, ambos com destruidores que o libertam ... causando um duplo livre , o que é um problema. NB. Isso seria perfeitamente normal, se fizéssemos uma cópia semântica do vem w, pois então wseria independente Vec<u8>e os destruidores não estariam atropelando uns aos outros.

Existem algumas soluções possíveis aqui:

  • Deixe o programador lidar com isso, como C. (não há destruidores em C, então não é tão ruim ... você apenas fica com vazamentos de memória em vez disso.: P)
  • Realiza uma cópia semântica implicitamente, de forma que wtenha sua própria alocação, como C ++ com seus construtores de cópia.
  • Considerar por valor usa como uma transferência de propriedade, de forma que vnão pode mais ser usado e não tem seu destruidor rodando.

O último é o que o Rust faz: um movimento é apenas um uso por valor em que a fonte é invalidada estaticamente, de modo que o compilador evita o uso futuro da memória agora inválida.

let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value

Tipos que têm destruidores devem se mover quando usados ​​por valor (também conhecido como quando byte copiado), uma vez que eles têm gerenciamento / propriedade de algum recurso (por exemplo, uma alocação de memória ou um identificador de arquivo) e é muito improvável que uma cópia de byte duplique isso corretamente propriedade.

"Bem ... o que é uma cópia implícita?"

Pense em um tipo primitivo como u8: uma cópia de byte é simples, basta copiar o byte único, e uma cópia semântica é tão simples, copiar o byte único. Em particular, uma cópia de byte é uma cópia semântica ... Rust ainda tem um traço embutidoCopy que captura quais tipos têm cópias semânticas e de byte idênticas.

Portanto, para esses Copytipos, os usos por valor também são cópias semânticas automaticamente e, portanto, é perfeitamente seguro continuar usando o código-fonte.

let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine

Antigo : O NoCopymarcador substitui o comportamento automático do compilador de assumir que os tipos que podem ser Copy(ou seja, contendo apenas agregados de primitivos e &) são Copy. No entanto, isso mudará quando as características incorporadas opcionais forem implementadas.

Conforme mencionado acima, características integradas opt-in são implementadas, portanto, o compilador não tem mais comportamento automático. No entanto, a regra usada para o comportamento automático no passado são as mesmas regras para verificar se é legal implementarCopy .

Huon
fonte
@dbaupp: Você saberia em qual versão do Rust as características opcionais incorporadas apareceram? Eu acho que 0,10.
Matthieu M.
@MatthieuM. ele ainda não foi implementado e, na verdade, recentemente houve algumas revisões propostas para o design de opções integradas .
huon
Eu acho que aquela citação antiga deveria ser apagada.
Stargateur
1
# [derivar (Copiar, Clonar)] deve ser usado no Triplet não impl
shadowbq
6

A maneira mais fácil é incorporar algo no seu tipo que não possa ser copiado.

A biblioteca padrão fornece um "tipo de marcador" exatamente para este caso de uso: NoCopy . Por exemplo:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}
BurntSushi5
fonte
15
Isso não é válido para Rust> = 1.0.
malbarbo