2015-04-07 10 views
24

Le affermazioni Rust language website spostano la semantica come una delle caratteristiche della lingua. Ma non riesco a vedere come si muove la semantica in Rust.In che modo Rust fornisce la semantica del movimento?

Le caselle di ruggine sono l'unico posto in cui vengono utilizzate le semantica del movimento.

let x = Box::new(5); 
let y: Box<i32> = x; // x is 'moved' 

Il codice di ruggine sopra può essere scritto in C++ come

auto x = std::make_unique<int>(); 
auto y = std::move(x); // Note the explicit move 

Per quanto ne so (correggetemi se sbaglio),

  • ruggine non ha costruttori a tutti, figuriamoci spostare i costruttori.
  • Nessun supporto per i riferimenti di rvalue.
  • Impossibile creare sovraccarichi di funzioni con i parametri rvalue.

In che modo Rust fornisce la semantica del movimento?

+3

Principalmente, laddove C++ copi implicitamente, Rust si sposta implicitamente. Questo non si applica solo alle scatole. – rightfold

risposta

29

Penso che sia un problema molto comune quando proviene dal C++. In C++ stai facendo tutto esplicitamente quando si tratta di copiare e spostare. Il linguaggio è stato progettato attorno a copie e riferimenti. Con C++ 11 la capacità di "spostare" le cose era incollata su quel sistema. La ruggine d'altra parte ha preso un nuovo inizio.


ruggine non ha costruttori a tutti, lasciare che i costruttori si muovono da soli.

Non è necessario spostare i costruttori. Rust sposta tutto ciò che "non ha un costruttore di copia", a.k.a. "non implementa il tratto Copy".

struct A; 

fn test() { 
    let a = A; 
    let b = a; 
    let c = a; // error, a is moved 
} 

costruttore di default di Rust è (per convenzione) semplicemente una funzione associata chiamato new:

struct A(i32); 
impl A { 
    fn new() -> A { 
     A(5) 
    } 
} 

più costruttori complessi dovrebbero avere nomi più espressivo. Questo è il linguaggio costruttore chiamato in C++


Nessun supporto per i riferimenti rvalue.

E 'sempre stata una caratteristica richiesta, vedi RFC issue 998, ma molto probabilmente si sta chiedendo per una caratteristica diversa: roba di trasferirsi a funzioni:

struct A; 

fn move_to(a: A) { 
    // a is moved into here, you own it now. 
} 

fn test() { 
    let a = A; 
    move_to(a); 
    let c = a; // error, a is moved 
} 

Non c'è modo di creare funzioni sovraccarichi con parametri rvalue.

È possibile farlo con tratti.

trait Ref { 
    fn test(&self); 
} 

trait Move { 
    fn test(self); 
} 

struct A; 
impl Ref for A { 
    fn test(&self) { 
     println!("by ref"); 
    } 
} 
impl Move for A { 
    fn test(self) { 
     println!("by value"); 
    } 
} 
fn main() { 
    let a = A; 
    (&a).test(); // prints "by ref" 
    a.test(); // prints "by value" 
} 
+4

Quindi in realtà manca una funzionalità di C++ o Rust lo sta facendo in modo diverso? –

+0

In realtà non mi manca alcuna funzionalità del C++, ma il modo in cui la semantica del movimento è implementata è troppo diversa. Sebbene il punto di Rust sia evitare le copie implicite, non mi viene in mente l'idea di "spostare i tipi di valore sull'assegnazione" quando possiamo usare i riferimenti. – user3335

+2

In ruggine invece di rendere esplicito lo spostamento, creare riferimenti è esplicito: 'let x = & a;' crea un riferimento (const) chiamato 'x' a' a'. Inoltre, dovresti fidarti del compilatore quando si tratta di ottimizzazioni nel caso in cui si teme che le mosse implicite creino una penalizzazione delle prestazioni. Il compilatore può ottimizzare molto a causa della semantica di spostamento che viene incorporata nel compilatore. –

1

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 è Copynon 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.

Problemi correlati