web-dev-qa-db-fra.com

Comment Rust fournit-il la sémantique des mouvements?

Le site Web en langue rouille prétend déplacer la sémantique comme l'une des caractéristiques de la langue. Mais je ne vois pas comment la sémantique de déplacement est implémentée dans Rust.

Les boîtes de rouille sont le seul endroit où la sémantique de mouvement est utilisée.

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

Le code Rust Rust peut être écrit en C++ comme

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

Pour autant que je sache (corrigez-moi si je me trompe),

  • La rouille n'a pas de constructeurs du tout, encore moins de déplacer des constructeurs.
  • Pas de support pour les références rvalue.
  • Aucun moyen de créer des surcharges de fonctions avec des paramètres rvalue.

Comment Rust fournit-il la sémantique des mouvements?

42
user3335

Je pense que c'est un problème très courant en venant de C++. En C++, vous faites tout explicitement quand il s'agit de copier et de déplacer. Le langage a été conçu autour de la copie et des références. Avec C++ 11, la possibilité de "déplacer" des trucs était collée sur ce système. Rust d'autre part a pris un nouveau départ.


La rouille n'a pas de constructeurs du tout, encore moins de déplacer des constructeurs.

Vous n'avez pas besoin de déplacer les constructeurs. Rust déplace tout ce qui "n'a pas de constructeur de copie", a.k.a. "n'implémente pas le trait Copy").

struct A;

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

Le constructeur par défaut de Rust est (par convention) simplement une fonction associée appelée new:

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

Les constructeurs plus complexes devraient avoir des noms plus expressifs. Il s'agit de l'idiome du constructeur nommé en C++


Pas de support pour les références rvalue.

Cela a toujours été une fonctionnalité demandée, voir RFC issue 998 , mais vous demandez très probablement une fonctionnalité différente: déplacer des éléments vers des fonctions:

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
}

Aucun moyen de créer des surcharges de fonctions avec des paramètres rvalue.

Vous pouvez le faire avec des traits.

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

La sémantique de déplacement et de copie de Rust est très différente de C++. Je vais adopter une approche différente pour les expliquer que la réponse existante.


En C++, la copie est une opération qui peut être arbitrairement complexe, en raison de constructeurs de copie personnalisés. Rust ne veut pas de sémantique personnalisée de simple affectation ou passage d'argument, et adopte donc une approche différente.

Tout d'abord, une affectation ou un argument passant Rust n'est toujours qu'une simple copie de mémoire.

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)

Mais que se passe-t-il si l'objet contrôle certaines ressources? Disons que nous avons affaire à un simple pointeur intelligent, Box.

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

À ce stade, si seuls les octets sont copiés, le destructeur (drop dans Rust) ne serait-il pas appelé pour chaque objet, libérant ainsi le même pointeur deux fois et provoquant un comportement indéfini?

La réponse est que Rust se déplace par défaut. Cela signifie qu'il copie les octets vers le nouvel emplacement, et le l'ancien objet a alors disparu. C'est une erreur de compilation pour accéder à b1 après la deuxième ligne ci-dessus. Et le destructeur n'est pas appelé pour cela. La valeur a été déplacée vers b2, et b1 pourrait aussi bien ne plus exister.

C'est ainsi que fonctionne la sémantique des mouvements dans Rust. Les octets sont copiés et l'ancien objet a disparu.

Dans certaines discussions sur la sémantique des mouvements de C++, Rust a été appelé "mouvement destructeur". Il a été proposé d'ajouter le "destructeur de mouvements" ou quelque chose de similaire au C++ afin qu'il puisse avoir la même sémantique. Mais déplacer la sémantique telle qu'elle est implémentée en C++ ne le fait pas. Le vieil objet est laissé derrière, et son destructeur est toujours appelé. Par conséquent, vous avez besoin d'un constructeur de déplacement pour gérer la logique personnalisée requise par l'opération de déplacement. Le déplacement n'est qu'un constructeur/opérateur d'affectation spécialisé qui devrait se comporter d'une certaine manière.


Par défaut, l'affectation de Rust déplace l'objet, rendant l'ancien emplacement invalide. Mais de nombreux types (entiers, virgules flottantes, références partagées) ont une sémantique où la copie des octets est un moyen parfaitement valide de créer une copie réelle, sans avoir besoin d'ignorer l'ancien objet. Ces types doivent implémenter le trait Copy, qui peut être dérivé automatiquement par le compilateur.

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

Cela signale au compilateur que l'affectation et le passage d'arguments n'invalident pas l'ancien objet:

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

Notez que la copie triviale et le besoin de destruction s'excluent mutuellement; un type qui est Copy ne peut pas être également Drop.


Maintenant, qu'en est-il lorsque vous voulez faire une copie de quelque chose où il ne suffit pas de copier les octets, par exemple un vecteur? Il n'y a pas de fonction linguistique pour cela; techniquement, le type a juste besoin d'une fonction qui renvoie un nouvel objet qui a été créé de la bonne façon. Mais par convention, ceci est réalisé en implémentant le trait Clone et sa fonction clone. En fait, le compilateur prend également en charge la dérivation automatique de Clone, où il clone simplement chaque champ.

#[Derive(Clone)]
struct JustTwoVecs {
  one: Vec<i32>,
  two: Vec<i32>,
}

let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();

Et chaque fois que vous dérivez Copy, vous devez également dériver Clone, car les conteneurs comme Vec l'utilisent en interne lorsqu'ils sont clonés eux-mêmes.

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

Maintenant, y a-t-il des inconvénients à cela? Oui, en fait, il y a un gros inconvénient: parce que le déplacement d'un objet vers un autre emplacement mémoire se fait simplement en copiant des octets, et sans logique personnalisée, un type ne peut pas avoir de références en lui-même . En fait, le système à vie de Rust ne permet pas de construire de tels types en toute sécurité.

Mais à mon avis, le compromis en vaut la peine.

16
Sebastian Redl

Rust prend en charge la sémantique de déplacement avec des fonctionnalités comme celles-ci:

  • Tous les types sont mobiles.

  • L'envoi d'une valeur quelque part est un mouvement, par défaut, dans toute la langue. Pour les types nonCopy, comme Vec, les mouvements suivants sont tous des mouvements dans Rust: passant un argument par valeur, renvoyant une valeur, affectation, correspondance de modèle par valeur.

    Vous n'avez pas std::move Dans Rust parce que c'est la valeur par défaut. Vous utilisez vraiment des mouvements tout le temps.

  • Rust sait que les valeurs déplacées ne doivent pas être utilisées. Si vous avez une valeur x: String Et faites channel.send(x), en envoyant la valeur à un autre thread, le compilateur sait que x a été déplacé. Essayer de l'utiliser après le déplacement est une erreur de compilation, "utilisation de la valeur déplacée". Et vous ne pouvez pas déplacer une valeur si quelqu'un y a une référence (un pointeur suspendu).

  • Rust sait qu'il ne faut pas appeler de destructeurs sur les valeurs déplacées. Le déplacement d'une valeur transfère la propriété, y compris la responsabilité du nettoyage. Il n'est pas nécessaire que les types puissent représenter un état spécial "la valeur a été déplacée".

  • Les mouvements sont bon marché et les performances sont prévisibles. C'est essentiellement memcpy. Renvoyer un énorme Vec est toujours rapide: vous copiez simplement trois mots.

  • La bibliothèque standard Rust utilise et prend en charge les déplacements partout. J'ai déjà mentionné les canaux, qui utilisent la sémantique de déplacement pour transférer en toute sécurité la propriété des valeurs sur les threads. Autres petites touches: tous les types supporte la copie sans fonction std::mem::swap() dans Rust; les traits de conversion standard Into et From sont par valeur; Vec et les autres collections ont .drain() et .into_iter() afin que vous puissiez détruire une structure de données, en retirer toutes les valeurs et utiliser ces valeurs pour en créer une nouvelle.

Rust n'a pas de références de mouvement, mais les mouvements sont un concept puissant et central dans Rust, offrant beaucoup des mêmes avantages en termes de performances qu'en C++, ainsi que d'autres avantages.

2
Jason Orendorff