Hoe biedt Rust bewegingssemantiek?

De Rust-taalwebsiteclaimt verplaatsingssemantiek als een van de kenmerken van de taal. Maar ik kan niet zien hoe de bewegingssemantiek is geïmplementeerd in Rust.

Rust boxes zijn de enige plaats waar move-semantiek wordt gebruikt.

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

De bovenstaande Rust-code kan in C++ worden geschreven als

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

Voor zover ik weet (corrigeer me als ik het mis heb),

  • Rust heeft helemaal geen constructors, laat staan ​​verplaats constructors.
  • Geen ondersteuning voor rvalu-referenties.
  • Geen manier om overbelasting van functies te creëren met rvalue-parameters.

Hoe biedt Rust bewegingssemantiek?


Antwoord 1, autoriteit 100%

Ik denk dat het een veel voorkomend probleem is als het uit C++ komt. In C++ doe je alles expliciet als het gaat om kopiëren en verplaatsen. De taal is ontworpen rond kopiëren en referenties. Met C++11 werd de mogelijkheid om dingen te “verplaatsen” op dat systeem gelijmd. Rust daarentegen nam een ​​nieuwe start.


Rust heeft helemaal geen constructors, laat staan ​​verplaats constructors.

U hebt geen move-constructors nodig. Rust verplaatst alles wat “geen copy-constructor heeft”, oftewel “de eigenschap Copyniet implementeert”.

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

De standaardconstructor van Rust is (volgens conventie) gewoon een bijbehorende functie genaamd new:

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

Meer complexe constructors zouden meer expressieve namen moeten hebben. Dit is het benoemde constructor-idioom in C++


Geen ondersteuning voor rvalu-referenties.

Het is altijd een gevraagde functie geweest, zie RFC-probleem 998, maar hoogstwaarschijnlijk vraag je om een ​​andere functie: dingen verplaatsen naar functies:

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
}

Geen manier om overbelasting van functies te creëren met rvalue-parameters.

Dat kun je doen met eigenschappen.

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"
}

Antwoord 2, autoriteit 62%

De semantiek van Rust voor verplaatsen en kopiëren is heel anders dan die van C++. Ik ga een andere benadering gebruiken om ze uit te leggen dan het bestaande antwoord.


In C++ is kopiëren een bewerking die willekeurig complex kan zijn vanwege aangepaste kopieerconstructors. Rust wil geen aangepaste semantiek van eenvoudige toewijzing of het doorgeven van argumenten, en kiest daarom voor een andere benadering.

Ten eerste is een opdracht of argument dat in Rust wordt doorgegeven, altijd een simpele geheugenkopie.

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)

Maar wat als het object bepaalde bronnen beheert? Laten we zeggen dat we te maken hebben met een eenvoudige slimme aanwijzer, Box.

let b1 = Box::new(42);
let b2 = b1;

Als nu alleen de bytes worden gekopieerd, zou dan niet de destructor (Dropin Rust) worden aangeroepen voor elk object, waardoor dezelfde aanwijzer twee keer wordt vrijgemaakt en ongedefinieerd gedrag wordt veroorzaakt?

Het antwoord is dat Rust standaard beweegt. Dit betekent dat het de bytes naar de nieuwe locatie kopieert, en het oude object is dan verdwenen. Het is een compileerfout om b1te openen na de tweede regel hierboven. En de vernietiger is er niet voor geroepen. De waarde is verplaatst naar b2en b1kan net zo goed niet meer bestaan.

Zo werkt de bewegingssemantiek in Rust. De bytes worden gekopieerd en het oude object is weg.

In sommige discussies over de bewegingssemantiek van C++ werd de manier van Rust “destructieve beweging” genoemd. Er zijn voorstellen gedaan om de “move destructor” of iets vergelijkbaars met C++ toe te voegen, zodat het dezelfde semantiek kan hebben. Maar verplaats semantiek zoals ze zijn geïmplementeerd in C++, doe dit niet. Het oude object blijft achter en de vernietiger wordt nog steeds genoemd. Daarom hebt u een move-constructor nodig om de aangepaste logica af te handelen die vereist is voor de verplaatsingsbewerking. Verplaatsen is slechts een gespecialiseerde constructor/toewijzingsoperator waarvan wordt verwacht dat deze zich op een bepaalde manier gedraagt.


Dus standaard verplaatst de toewijzing van Rust het object, waardoor de oude locatie ongeldig wordt. Maar veel typen (gehele getallen, zwevende punten, gedeelde referenties) hebben een semantiek waarbij het kopiëren van de bytes een perfect geldige manier is om een ​​echte kopie te maken, zonder dat het oude object hoeft te worden genegeerd. Dergelijke typen zouden de eigenschap Copymoeten implementeren, die automatisch door de compiler kan worden afgeleid.

#[derive(Copy)]
struct JustTwoInts {
  one: i32,
  two: i32,
}

Dit signaleert de compiler dat toewijzing en het doorgeven van argumenten het oude object niet ongeldig maakt:

let j1 = JustTwoInts { one: 1, two: 2 };
let j2 = j1;
println!("Still allowed: {}", j1.one);

Merk op dat triviaal kopiëren en de noodzaak van vernietiging elkaar uitsluiten; een type dat Copyis kan nietook Dropzijn.


Hoe zit het nu als je een kopie wilt maken van iets waar alleen het kopiëren van de bytes niet genoeg is, b.v. een vector? Hier is geen taalfunctie voor; technisch gezien heeft het type alleen een functie nodig die een nieuw object retourneert dat op de juiste manier is gemaakt. Maar volgens afspraak wordt dit bereikt door de eigenschap Cloneen zijn functie Clonete implementeren. In feite ondersteunt de compiler ook automatische afleiding van Clone, waarbij het simpelweg elk veld kloont.

#[Derive(Clone)]
struct JustTwoVecs {
  one: Vec<i32>,
  two: Vec<i32>,
}
let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();

En wanneer u Copyafleidt, moet u ook Cloneafleiden, omdat containers zoals Vechet intern gebruiken wanneer ze zelf worden gekloond.

#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }

Nou, zijn hier nadelen aan verbonden? Ja, in feite is er één nogal groot nadeel: omdat het verplaatsen van een object naar een andere geheugenlocatie gewoon wordt gedaan door bytes te kopiëren, en geen aangepaste logica, een type kan geen verwijzingen in zichzelf hebben. In feite maakt het levenslange systeem van Rust het onmogelijk om dergelijke typen veilig te bouwen.

Maar naar mijn mening is de afweging de moeite waard.


Antwoord 3, autoriteit 20%

Rust ondersteunt verplaatsingssemantiek met functies als deze:

  • Alle typen zijn verplaatsbaar.

  • Het ergens naartoe sturen van een waarde is standaard een verplaatsing in de hele taal.Voor niet-Copy-typen, zoals Vec, de volgende zijn allemaal zetten in Rust: een argument doorgeven op waarde, een waarde retourneren, toewijzing, patroonovereenkomst op waarde.

    Je hebt std::moveniet in Rust omdat dit de standaard is. Je gebruikt echt constant bewegingen.

  • Rust weet dat verplaatste waarden niet mogen worden gebruikt.Als je een waarde x: Stringhebt en channel.send(x), de waarde naar een andere thread sturend, weet de compiler dat xis verplaatst. Het proberen te gebruiken na de verplaatsing is een compile-time-fout, “gebruik van verplaatste waarde”. En je kunt een waarde niet verplaatsen als iemand er een verwijzing naar heeft (een bungelende aanwijzer).

  • Rust weet dat hij geen vernietigers moet bellen voor verplaatste waarden.Door een waarde te verplaatsen, wordt het eigendom overgedragen, inclusief de verantwoordelijkheid voor het opruimen. Typen hoeven geen speciale status “waarde is verplaatst” te kunnen vertegenwoordigen.

  • Verhuizingen zijn goedkoopen de prestaties zijn voorspelbaar. Het is eigenlijk memcpy. Het retourneren van een enorme Vecgaat altijd snel: je kopieert slechts drie woorden.

  • De Rust-standaardbibliotheek gebruikt en ondersteunt overal verplaatsingen.Ik noemde al kanalen, die verplaatsingssemantiek gebruiken om het eigendom van waarden over threads veilig over te dragen. Andere leuke details: alle typen ondersteunen kopieervrije std::mem::swap()in Rust; de standaard conversiekenmerken Intoen Fromzijn op waarde; Vecen andere collecties hebben de methoden .drain()en .into_iter()zodat u één gegevensstructuur kunt vernietigen, alle waarden uit de en gebruik die waarden om een ​​nieuwe te bouwen.

Rust heeft geen verplaatsingsreferenties, maar bewegingen zijn een krachtig en centraal concept in Rust, dat veel van dezelfde prestatievoordelen biedt als in C++, en ook enkele andere voordelen.


Antwoord 4

Ik wil hieraan toevoegen dat het niet nodig is om naar memcpyte gaan. Als het object op de stapel groot genoeg is, kan de compiler van Rust ervoor kiezen om de aanwijzer van het object door te geven.


Antwoord 5

In C++ is de standaardtoewijzing van klassen en structs een oppervlakkige kopie. De waarden worden gekopieerd, maar niet de gegevens waarnaar door verwijzingen wordt verwezen. Als u dus één instantie wijzigt, worden de gegevens waarnaar wordt verwezen, van alle kopieën gewijzigd. De waarden (bijv. gebruikt voor administratie) blijven in het andere geval ongewijzigd, wat waarschijnlijk een inconsistente toestand oplevert. Een bewegingssemantiek vermijdt deze situatie. Voorbeeld voor een C++-implementatie van een geheugenbeheerde container met verplaatsingssemantiek:

template <typename T>
class object
{
    T *p;
public:
    object()
    {
        p=new T;
    }
    ~object()
    {
        if (p != (T *)0) delete p;
    }
    template <typename V> //type V is used to allow for conversions between reference and value
    object(object<V> &v)      //copy constructor with move semantic
    {
        p = v.p;      //move ownership
        v.p = (T *)0; //make sure it does not get deleted
    }
    object &operator=(object<T> &v) //move assignment
    {
        delete p;
        p = v.p;
        v.p = (T *)0;
        return *this;
    }
    T &operator*() { return *p; } //reference to object  *d
    T *operator->() { return p; } //pointer to object data  d->
};

Zo’n object wordt automatisch verzameld en kan vanuit functies worden teruggestuurd naar het aanroepende programma. Het is extreem efficiënt en doet hetzelfde als Rust:

object<somestruct> somefn() //function returning an object
{
   object<somestruct> a;
   auto b=a;  //move semantic; b becomes invalid
   return b;  //this moves the object to the caller
}
auto c=somefn();
//now c owns the data; memory is freed after leaving the scope

Other episodes