È possibile rendere un tipo solo mobile e non copibile?


96

Nota dell'editore : questa domanda è stata posta prima di Rust 1.0 e alcune delle asserzioni nella domanda non sono necessariamente vere in Rust 1.0. Alcune risposte sono state aggiornate per affrontare entrambe le versioni.

Ho questa struttura

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

Se lo passo a una funzione, viene copiato implicitamente. Ora, a volte leggo che alcuni valori non sono copiabili e quindi devono essere spostati.

Sarebbe possibile rendere questa struttura Tripletnon copiabile? Ad esempio, sarebbe possibile implementare un tratto che renderebbe Tripletnon copiabili e quindi "mobili"?

Ho letto da qualche parte che si deve implementare il Clonetratto per copiare cose che non sono copiabili implicitamente, ma non ho mai letto il contrario, ovvero avere qualcosa che è implicitamente copiabile e renderlo non copiabili in modo che si muova invece.

Ha anche senso?


1
paulkoerbitz.de/posts/… . Buone spiegazioni qui del motivo per cui spostare e copiare.
Sean Perry

Risposte:


165

Premessa : Questa risposta è stata scritta prima di opt-in incorporato tratti -specifically gli Copyaspetti -Ve implementate. Ho usato le virgolette per indicare le sezioni che si applicavano solo al vecchio schema (quello che si applicava quando è stata posta la domanda).


Vecchio : per rispondere alla domanda di base, puoi aggiungere un campo indicatore che memorizza un NoCopyvalore . Per esempio

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

Puoi anche farlo con un distruttore (tramite l'implementazione del Droptratto ), ma è preferibile usare i tipi di marker se il distruttore non sta facendo nulla.

I tipi ora si spostano per impostazione predefinita, ovvero quando definisci un nuovo tipo non viene implementato a Copymeno che non lo implementi esplicitamente per il tuo tipo:

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

L'implementazione può esistere solo se ogni tipo contenuto nel nuovo structo enumè esso stesso Copy. In caso contrario, il compilatore stamperà un messaggio di errore. Può anche esistere solo se il tipo non ha Dropun'implementazione.


Per rispondere alla domanda che non hai posto ... "che succede con le mosse e la copia?":

Per prima cosa definirò due diverse "copie":

  • una copia in byte , che sta solo copiando superficialmente un oggetto byte per byte, non seguendo i puntatori, ad esempio se lo hai (&usize, u64), è di 16 byte su un computer a 64 bit, e una copia superficiale prenderebbe quei 16 byte e replicherebbe i loro valore in qualche altro blocco di memoria da 16 byte, senza toccare usizel'altra estremità del file &. Cioè, è equivalente a chiamare memcpy.
  • una copia semantica , duplicando un valore per creare una nuova istanza (in qualche modo) indipendente che può essere tranquillamente utilizzata separatamente da quella vecchia. Ad esempio, una copia semantica di un Rc<T>comporta solo l'aumento del conteggio dei riferimenti, e una copia semantica di a Vec<T>implica la creazione di una nuova allocazione, e quindi la copia semantica di ogni elemento memorizzato dal vecchio al nuovo. Questi possono essere copie profonde (ad esempio Vec<T>) o superficiali (ad esempio Rc<T>, non toccano ciò che è memorizzato T), Cloneè definito liberamente come la più piccola quantità di lavoro richiesta per copiare semanticamente un valore di tipo Tda dentro a &Ta T.

Rust è come C, ogni uso per valore di un valore è una copia in byte:

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

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

foo(y) // byte copy

Sono copie in byte indipendentemente dal fatto che si Tmuovano o che siano "copiabili implicitamente". (Per essere chiari, non sono necessariamente copie letteralmente byte per byte in fase di esecuzione: il compilatore è libero di ottimizzare le copie se il comportamento del codice viene preservato.)

Tuttavia, c'è un problema fondamentale con le copie di byte: si finisce con valori duplicati in memoria, il che può essere pessimo se hanno distruttori, ad es.

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

Se wfosse solo una semplice copia in byte di vallora ci sarebbero due vettori che puntano alla stessa allocazione, entrambi con distruttori che lo liberano ... causando un doppio libero , il che è un problema. NB. Sarebbe perfettamente a posto, se facessimo una copia semantica di vinto w, da allora wsarebbe indipendente Vec<u8>ei distruttori non si calpesterebbero a vicenda.

Ci sono alcune possibili soluzioni qui:

  • Lascia che sia il programmatore a gestirlo, come C. (non ci sono distruttori in C, quindi non è così male ... invece ti rimangono solo perdite di memoria.: P)
  • Esegui una copia semantica in modo implicito, in modo che wabbia la sua allocazione, come C ++ con i suoi costruttori di copia.
  • Considera gli usi per valore come un trasferimento di proprietà, in modo che vnon possa più essere utilizzato e non abbia il suo distruttore in esecuzione.

L'ultimo è ciò che fa Rust: una mossa è solo un uso per valore in cui il sorgente è staticamente invalidato, quindi il compilatore impedisce un ulteriore utilizzo della memoria ora non valida.

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

I tipi che hanno distruttori devono spostarsi quando usati per valore (ovvero quando i byte copiati), poiché hanno la gestione / proprietà di alcune risorse (ad esempio un'allocazione di memoria o un handle di file) ed è molto improbabile che una copia di byte lo duplichi correttamente Proprietà.

"Beh ... cos'è una copia implicita?"

Pensa a un tipo primitivo come u8: una copia di byte è semplice, basta copiare il singolo byte e una copia semantica è altrettanto semplice, copiare il singolo byte. In particolare, una copia di byte è una copia semantica ... Rust ha anche una caratteristica incorporataCopy che cattura quali tipi hanno copie semantiche e byte identiche.

Quindi, per questi Copytipi gli usi in base al valore sono automaticamente anche copie semantiche, quindi è perfettamente sicuro continuare a utilizzare il sorgente.

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

Vecchio : il NoCopymarcatore sovrascrive il comportamento automatico del compilatore di assumere che i tipi che possono essere Copy(cioè contenenti solo aggregati di primitive e &) lo siano Copy. Tuttavia, questo cambierà quando verranno implementati i tratti incorporati di opt-in .

Come accennato in precedenza, vengono implementati i tratti incorporati opt-in, quindi il compilatore non ha più un comportamento automatico. Tuttavia, le regole utilizzate in passato per il comportamento automatico sono le stesse regole per verificare se è legale da implementare Copy.


@dbaupp: sapresti per caso in quale versione di Rust compaiono i tratti incorporati di opt-in? Penso che 0.10.
Matthieu M.

@MatthieuM. non è ancora implementato e di recente sono state proposte alcune revisioni al design dei built-in opt-in .
huon

Penso che quella vecchia citazione dovrebbe essere cancellata.
Stargateur

1
# [derive (Copy, Clone)] dovrebbe essere usato su Triplet non
impl

6

Il modo più semplice è incorporare qualcosa nel tuo tipo che non è copiabili.

La libreria standard fornisce un "tipo di marker" proprio per questo caso d'uso: NoCopy . Per esempio:

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

15
Questo non è valido per Rust> = 1.0.
malbarbo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.