web-dev-qa-db-fra.com

Est-il possible de rendre un type uniquement mobile et non copiable?

NDLR: cette question a été posée avant Rust 1.0 et certaines des affirmations de la question ne sont pas nécessairement vraies dans Rust 1.0 Certaines réponses ont été mises à jour pour répondre aux deux versions.

J'ai cette struct

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

Si je passe cela à une fonction, elle est implicitement copiée. Maintenant, parfois je lis que certaines valeurs ne sont pas copiables et doivent donc être déplacées.

Serait-il possible de rendre cette structure Triplet non copiable? Par exemple, serait-il possible d'implémenter un trait qui rendrait Triplet non copiable et donc "mobile"?

J'ai lu quelque part que l'on doit implémenter le trait Clone pour copier des choses qui ne sont pas implicitement copiables, mais je n'ai jamais lu l'inverse, c'est d'avoir quelque chose qui est implicitement copiable et de le rendre non copiable afin qu'il bouge à la place.

Cela a-t-il même un sens?

92
Christoph

Préface: Cette réponse a été écrite avant traits intégrés opt-in - spécifiquement les aspects Copy - ont été implémentés . J'ai utilisé des guillemets pour indiquer les sections qui ne s'appliquaient qu'à l'ancien schéma (celui qui s'appliquait lorsque la question a été posée).


Ancien : Pour répondre à la question de base, vous pouvez ajouter un champ marqueur stockant une valeur NoCopy . Par exemple.

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

Vous pouvez également le faire en ayant un destructeur (via l'implémentation du trait Drop ), mais l'utilisation des types de marqueurs est préférable si le destructeur ne fait rien.

Les types se déplacent désormais par défaut, c'est-à-dire que lorsque vous définissez un nouveau type, il n'implémente pas Copy sauf si vous l'implémentez explicitement pour votre type:

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

L'implémentation ne peut exister que si chaque type contenu dans le nouveau struct ou enum est lui-même Copy. Sinon, le compilateur affichera un message d'erreur. Il ne peut également exister que si le type n'existe pas a une implémentation Drop.


Pour répondre à la question que vous n'avez pas posée ... "Qu'en est-il des mouvements et de la copie?":

Je définirai d'abord deux "copies" différentes:

  • a copie d'octets, qui copie simplement superficiellement un objet octet par octet, sans suivre les pointeurs, par ex. si tu as (&usize, u64), il s'agit de 16 octets sur un ordinateur 64 bits, et une copie superficielle prendrait ces 16 octets et répliquerait leur valeur dans un autre bloc de mémoire de 16 octets, sans toucher le usize à l'autre bout du &. Autrement dit, cela équivaut à appeler memcpy.
  • a copie sémantique, duplication d'une valeur pour créer une nouvelle instance (quelque peu) indépendante qui peut être utilisée en toute sécurité séparément de l'ancienne. Par exemple. une copie sémantique d'un Rc<T> implique simplement d'augmenter le nombre de références, et une copie sémantique d'un Vec<T> implique la création d'une nouvelle allocation, puis la copie sémantique de chaque élément stocké de l'ancien vers le nouveau. Il peut s'agir de copies complètes (par exemple Vec<T>) ou peu profond (par exemple Rc<T> ne touche pas le stocké T), Clone est défini de manière lâche comme la plus petite quantité de travail requise pour copier sémantiquement une valeur de type T depuis l'intérieur d'un &T à T.

La rouille est comme C, chaque par valeur, l'utilisation d'une valeur est une copie d'octets:

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

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

foo(y) // byte copy

Ce sont des copies d'octets, que T se déplace ou soit "implicitement copiable". (Pour être clair, ils ne sont pas nécessairement des copies littéralement octet par octet au moment de l'exécution: le compilateur est libre d'optimiser les copies si le comportement du code est préservé.)

Cependant, il y a un problème fondamental avec les copies d'octets: vous vous retrouvez avec des valeurs dupliquées en mémoire, ce qui peut être très mauvais s'ils ont des destructeurs, par ex.

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

Si w n'était qu'une copie en octets simples de v, alors il y aurait deux vecteurs pointant vers la même allocation, tous deux avec des destructeurs qui la libèrent ... provoquant ne double liberté , ce qui est un problème. NB. Ce serait parfaitement bien, si nous faisions une copie sémantique de v dans w, puisque w serait alors son propre Vec<u8> et les destructeurs ne se piétineraient pas.

Il y a quelques correctifs possibles ici:

  • Laissez le programmeur s'en occuper, comme C. (il n'y a pas de destructeurs en C, donc ce n'est pas aussi mauvais ... il vous reste juste des fuites de mémoire à la place.: P)
  • Effectuer une copie sémantique implicitement, de sorte que w ait sa propre allocation, comme C++ avec ses constructeurs de copie.
  • Considérez les utilisations par valeur comme un transfert de propriété, de sorte que v ne peut plus être utilisé et que son destructeur ne soit pas exécuté.

Le dernier est ce que Rust fait: a déplacer est juste une utilisation par valeur où la source est statiquement invalidée, donc le compilateur empêche l'utilisation ultérieure du now -mémoire non valide.

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

Les types qui ont des destructeurs doivent se déplacent lorsqu'ils sont utilisés par valeur (aka lorsque l'octet est copié), car ils ont la gestion/la propriété de certaines ressources (par exemple une allocation de mémoire ou un descripteur de fichier) il est peu probable qu'une copie d'octets reproduise correctement cette propriété.

"Eh bien ... qu'est-ce qu'une copie implicite?"

Pensez à un type primitif comme u8: une copie d'octet est simple, copiez simplement le seul octet, et une copie sémantique est tout aussi simple, copiez le seul octet. En particulier, une copie d'octets est une copie sémantique ... Rust a même un trait intégré Copy) qui capture quels types ont des copies sémantiques et d'octets identiques.

Par conséquent, pour ces types de Copy, les utilisations par valeur sont également automatiquement des copies sémantiques, et il est donc parfaitement sûr de continuer à utiliser la source.

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

Ancien : le marqueur NoCopy remplace le comportement automatique du compilateur en supposant que les types qui peuvent être Copy (c'est-à-dire contenant uniquement agrégats de primitives et &) sont Copy. Cependant, cela changera lorsque traits intégrés opt-in est implémenté.

Comme mentionné ci-dessus, les traits intégrés opt-in sont implémentés, donc le compilateur n'a plus de comportement automatique. Cependant, les règles utilisées pour le comportement automatique dans le passé sont les mêmes règles pour vérifier s'il est légal d'implémenter Copy.

162
huon

Le moyen le plus simple consiste à intégrer quelque chose dans votre type qui n'est pas copiable.

La bibliothèque standard fournit un "type de marqueur" pour exactement ce cas d'utilisation: NoCopy . Par exemple:

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