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

96

Note de l'éditeur : 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 structure

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

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

Serait-il possible de rendre cette structure Tripletnon copiable? Par exemple, serait-il possible de mettre en œuvre un trait qui rendrait Tripletnon copiable et donc «déplaçable»?

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

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

Christoph
la source
1
paulkoerbitz.de/posts/… . De bonnes explications ici sur les raisons pour lesquelles déplacer par rapport à copier.
Sean Perry

Réponses:

164

Préface : Cette réponse a été rédigée avant la mise en œuvre des caractéristiques intégrées opt-in (en particulier les Copyaspects) . 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 était posée).


Ancien : pour répondre à la question de base, vous pouvez ajouter un champ de marqueur stockant une NoCopyvaleur . 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 Droptrait ), mais l'utilisation des types de marqueurs est préférable si le destructeur ne fait rien.

Les types se déplacent maintenant par défaut, c'est-à-dire que lorsque vous définissez un nouveau type, il n'implémente pas à Copymoins que vous ne l'implémentiez 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 structou enumest lui-même Copy. Sinon, le compilateur imprimera un message d'erreur. Il ne peut également exister que si le type n'existe pas d' Dropimplémentation.


Pour répondre à la question que vous n'avez pas posée ... "Que se passe-t-il avec les mouvements et la copie?":

Tout d'abord, je définirai deux "copies" différentes:

  • une copie d'octets , qui est juste une copie superficielle d'un objet octet par octet, sans suivre les pointeurs, par exemple si vous avez (&usize, u64), c'est 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 usizeà l'autre extrémité du &. Autrement dit, cela équivaut à appeler memcpy.
  • une copie sémantique , dupliquant 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 de an Rc<T>implique simplement d'augmenter le nombre de références, et une copie sémantique de a 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. Ceux-ci peuvent être des copies profondes (par exemple Vec<T>) ou superficielles (par exemple, Rc<T>ne touche pas le stocké T), Cloneest défini de manière approximative comme la plus petite quantité de travail requise pour copier sémantiquement une valeur de type Tde l'intérieur de a &Tà T.

Rust est comme C, chaque utilisation par valeur 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, qu'elles soient ou non T déplacées ou "implicitement copiables". (Pour être clair, ils ne sont pas nécessairement des copies octet par octet au moment de l'exécution: le compilateur est libre d'optimiser les copies si le comportement du code est conservé.)

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 si elles ont des destructeurs, par exemple

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

Si wc'était juste une copie d'octet simple de valors il y aurait deux vecteurs pointant vers la même allocation, tous deux avec des destructeurs qui la libèrent ... provoquant un double free , ce qui est un problème. NB. Ce serait parfaitement bien, si nous faisions une copie sémantique de vinto w, puisque ce wserait alors son propreVec<u8> et les destructeurs ne se piétineraient pas les uns les autres.

Il y a quelques correctifs possibles ici:

  • Laissez le programmeur le gérer, comme C. (il n'y a pas de destructeurs en C, donc ce n'est pas aussi grave ... vous vous retrouvez juste avec des fuites de mémoire à la place.: P)
  • Effectuez une copie sémantique implicitement, de sorte qu'elle wait 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 vcela ne puisse plus être utilisé et que son destructeur ne soit pas exécuté.

Le dernier est ce que fait Rust: un déplacement est juste une utilisation par valeur où la source est statiquement invalidée, de sorte que le compilateur empêche toute utilisation ultérieure de la mémoire désormais invalide.

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éplacer lorsqu'ils sont utilisés par valeur (c'est-à-dire 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) et il est très peu probable qu'une copie d'octet duplique correctement cela la possession.

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

Pensez à un type primitif comme u8: une copie d'octet est simple, copiez simplement l'octet unique, et une copie sémantique est tout aussi simple, copiez l'octet unique. En particulier, une copie d'octet 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 Copytypes, 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

Old : Le NoCopymarqueur remplace le comportement automatique du compilateur en supposant que les types qui peuvent être Copy(c'est-à-dire ne contenant que des agrégats de primitives et &) le sont Copy. Cependant, cela changera lorsque les caractéristiques intégrées opt-in seront implémentées.

Comme mentionné ci-dessus, les caractéristiques intégrées opt-in sont implémentées, de sorte que le compilateur n'a plus de comportement automatique. Cependant, la règle utilisée pour le comportement automatique dans le passé sont les mêmes règles pour vérifier si sa mise en œuvre est légale Copy.

huon
la source
@dbaupp: Savez-vous dans quelle version de Rust les traits intégrés opt-in sont apparus? Je penserais 0,10.
Matthieu M.
@MatthieuM. il n'est pas encore implémenté, et il y a en fait récemment quelques révisions proposées à la conception des intégrés opt-in .
huon
Je pense que cette vieille citation devrait être effacée.
Stargateur
1
# [derive (Copy, Clone)] devrait être utilisé sur Triplet not
impl
6

Le moyen le plus simple est d'incorporer dans votre texte quelque chose 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,
}
BurntSushi5
la source
15
Ce n'est pas valable pour Rust> = 1.0.
malbarbo