La semantica di spostamento e copia di Rust è molto diversa dal C++. Ho intenzione di adottare un approccio diverso per spiegarli rispetto alla risposta esistente.
In C++, la copia è un'operazione che può essere arbitrariamente complessa, a causa di costruttori di copia personalizzati. Rust non vuole una semantica personalizzata di semplice assegnazione o passaggio di argomenti, e quindi ha un approccio diverso.
In primo luogo, un compito o argomento che passa in Rust è sempre solo una semplice copia di memoria.
let foo = bar; // copies the bytes of bar to the location of foo (might be elided)
function(foo); // copies the bytes of foo to the parameter location (might be elided)
Ma cosa succede se l'oggetto controlla alcune risorse? Diciamo che abbiamo a che fare con un semplice puntatore intelligente, Box
.
let b1 = Box::new(42);
let b2 = b1;
A questo punto, se solo i byte vengono copiati, non avrebbe il distruttore (drop
in ruggine) essere chiamato per ogni oggetto, liberando così lo stesso puntatore due volte e causando comportamento indefinito?
La risposta è che Rust si sposta per impostazione predefinita. Ciò significa che copia i byte nella nuova posizione e il vecchio oggetto non è più disponibile. È un errore di compilazione per accedere a b1
dopo la seconda riga in alto. E il distruttore non è chiamato per questo. Il valore è stato spostato su b2
e b1
potrebbe non esistere più.
Questo è il modo in cui la semantica del movimento funziona in Rust. I byte vengono copiati e il vecchio oggetto è scomparso.
In alcune discussioni sulla semantica del movimento del C++, il modo in cui Rust si chiamava "mossa distruttiva". Ci sono state proposte per aggiungere "move destructor" o qualcosa di simile a C++ in modo che possa avere la stessa semantica. Ma spostare la semantica come sono implementate in C++ non farlo. Il vecchio oggetto viene lasciato indietro e viene ancora chiamato il suo distruttore. Pertanto, è necessario un costruttore di mosse per gestire la logica personalizzata richiesta dall'operazione di spostamento. Lo spostamento è solo un operatore specializzato di costruzione/assegnazione che si prevede si comporti in un certo modo.
Quindi, per impostazione predefinita, l'assegnazione di Rust sposta l'oggetto, rendendo invalida la vecchia posizione. Ma molti tipi (interi, punti mobili, riferimenti condivisi) hanno una semantica dove copiare i byte è un modo perfettamente valido per creare una copia reale, senza bisogno di ignorare il vecchio oggetto. Tali tipi dovrebbero implementare il tratto Copy
, che può essere derivato automaticamente dal compilatore.
#[derive(Copy)]
struct JustTwoInts {
one: i32,
two: i32,
}
Questo segnala al compilatore che l'assegnazione e l'argomento di passaggio non inficiano il vecchio oggetto:
let j1 = JustTwoInts { one: 1, two: 2 };
let j2 = j1;
println!("Still allowed: {}", j1.one);
Si noti che la copia banale e la necessità per la distruzione si escludono a vicenda; un tipo che è Copy
non può essereDrop
.
Ora, per quanto riguarda quando si desidera eseguire una copia di qualcosa in cui la copia dei byte non è sufficiente, ad es. un vettore?Non ci sono funzionalità linguistiche per questo; tecnicamente, il tipo ha solo bisogno di una funzione che restituisca un nuovo oggetto che è stato creato nel modo giusto. Ma per convenzione ciò si ottiene implementando il tratto Clone
e la sua funzione clone
. Infatti, il compilatore supporta la derivazione automatica di Clone
, dove semplicemente clona tutti i campi.
#[Derive(Clone)]
struct JustTwoVecs {
one: Vec<i32>,
two: Vec<i32>,
}
let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();
E ogni volta che si deriva Copy
, si dovrebbe anche derivare Clone
, perché contenitori come Vec
usano internamente quando essi stessi vengono clonati.
#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }
Ora, ci sono aspetti negativi di questo? Sì, in effetti c'è uno svantaggio piuttosto grande: perché lo spostamento di un oggetto in un'altra posizione di memoria avviene semplicemente copiando byte e senza logica personalizzata, un tipo cannot have references into itself. In effetti, il sistema a vita di Rust rende impossibile costruire tali tipi in modo sicuro.
Ma a mio parere, il trade-off è valsa la pena.
Principalmente, laddove C++ copi implicitamente, Rust si sposta implicitamente. Questo non si applica solo alle scatole. – rightfold