Ist es möglich, einen Typ nur beweglich und nicht kopierbar zu machen?

96

Anmerkung des Herausgebers : Diese Frage wurde vor Rust 1.0 gestellt, und einige der Aussagen in der Frage sind in Rust 1.0 nicht unbedingt wahr. Einige Antworten wurden aktualisiert, um beide Versionen zu adressieren.

Ich habe diese Struktur

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

Wenn ich dies an eine Funktion übergebe, wird es implizit kopiert. Jetzt habe ich manchmal gelesen, dass einige Werte nicht kopierbar sind und daher verschoben werden müssen.

Wäre es möglich, diese Struktur Tripletnicht kopierbar zu machen ? Wäre es beispielsweise möglich, ein Merkmal zu implementieren, das Tripletnicht kopierbar und daher "beweglich" wäre?

Ich habe irgendwo gelesen, dass man das CloneMerkmal implementieren muss, um Dinge zu kopieren, die nicht implizit kopierbar sind, aber ich habe nie darüber gelesen, dass etwas implizit kopierbar ist und es nicht kopierbar ist, sodass es sich stattdessen bewegt.

Macht das überhaupt Sinn?

Christoph
quelle
1
paulkoerbitz.de/posts/… . Hier finden Sie gute Erklärungen, warum Sie sich bewegen oder kopieren.
Sean Perry

Antworten:

164

Vorwort : Diese Antwort wurde geschrieben , bevor Opt-in - built-in Zügen -specifically die CopyAspekte umgesetzt -were. Ich habe Blockzitate verwendet, um die Abschnitte anzugeben, die nur für das alte Schema galten (den, der galt, als die Frage gestellt wurde).


Alt : Um die grundlegende Frage zu beantworten, können Sie ein Markierungsfeld hinzufügen, in dem ein NoCopyWert gespeichert ist . Z.B

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

Sie können dies auch tun, indem Sie einen Destruktor haben (indem Sie das DropMerkmal implementieren ). Die Verwendung der Markertypen wird jedoch bevorzugt, wenn der Destruktor nichts tut.

Typen werden jetzt standardmäßig verschoben, dh wenn Sie einen neuen Typ definieren, wird dieser nur implementiert, Copywenn Sie ihn explizit für Ihren Typ implementieren:

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

Die Implementierung kann nur existieren, wenn jeder Typ im neuen enthalten ist structoder enumselbst ist Copy. Wenn nicht, gibt der Compiler eine Fehlermeldung aus. Es kann auch nur existieren , wenn der Typ nicht funktioniert eine hat DropImplementierung.


Um die Frage zu beantworten, die Sie nicht gestellt haben ... "Was ist mit Zügen und Kopieren los?":

Zunächst definiere ich zwei verschiedene "Kopien":

  • Eine Byte-Kopie , die ein Objekt nur flach byteweise kopiert und keinen Zeigern folgt, z. B. wenn (&usize, u64)dies der Fall ist, sind es 16 Bytes auf einem 64-Bit-Computer, und eine flache Kopie würde diese 16 Bytes nehmen und ihre replizieren Wert in einem anderen 16-Byte-Speicherblock, ohne den usizeam anderen Ende des zu berühren &. Das heißt, es ist gleichbedeutend mit einem Anruf memcpy.
  • Eine semantische Kopie , die einen Wert dupliziert, um eine neue (etwas) unabhängige Instanz zu erstellen, die sicher getrennt von der alten verwendet werden kann. Bei einer semantischen Kopie von a wird beispielsweise Rc<T>nur die Referenzanzahl erhöht, und bei einer semantischen Kopie von a wird Vec<T>eine neue Zuordnung erstellt und anschließend jedes gespeicherte Element semantisch vom alten in das neue kopiert. Dies können tiefe Kopien (z. B. Vec<T>) oder flache Kopien sein (z. B. Rc<T>berührt das Gespeicherte nicht T). Dies Cloneist lose definiert als der kleinste Arbeitsaufwand, der erforderlich ist, um einen Wert vom Typ Tinnerhalb von a &Tnach semantisch zu kopieren T.

Rust ist wie C, jede Verwendung eines Werts nach Wert ist eine Bytekopie:

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

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

foo(y) // byte copy

Sie sind Byte-Kopien, unabhängig davon, ob sie Tverschoben werden oder "implizit kopierbar" sind. (Um klar zu sein, es handelt sich nicht unbedingt um buchstäblich byteweise Kopien zur Laufzeit: Der Compiler kann die Kopien optimieren, wenn das Verhalten des Codes erhalten bleibt.)

Bei Byte-Kopien gibt es jedoch ein grundlegendes Problem: Sie haben doppelte Werte im Speicher, die sehr schlecht sein können, wenn sie Destruktoren haben, z

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

Wenn wnur eine einfache Bytekopie von vwäre, würden zwei Vektoren auf dieselbe Zuordnung zeigen, beide mit Destruktoren, die sie freigeben ... was eine doppelte Freigabe verursacht , was ein Problem ist. NB. Dies wäre vollkommen in Ordnung, wenn wir eine semantische Kopie von vin machen würden w, da dies dann wseine eigene Unabhängigkeit wäre Vec<u8>und die Destruktoren sich nicht gegenseitig mit Füßen treten würden .

Hier gibt es einige mögliche Korrekturen:

  • Lassen Sie den Programmierer damit umgehen, wie C. (es gibt keine Destruktoren in C, also ist es nicht so schlimm ... Sie haben stattdessen nur Speicherlecks .: P)
  • Führen Sie implizit eine semantische Kopie durch, sodass diese weine eigene Zuordnung hat, wie z. B. C ++ mit ihren Kopierkonstruktoren.
  • Betrachten Sie By-Value-Verwendungen als Eigentumsübertragung, damit diese vnicht mehr verwendet werden können und der Destruktor nicht ausgeführt wird.

Das Letzte ist, was Rust tut: Eine Verschiebung ist nur eine Verwendung nach Wert, bei der die Quelle statisch ungültig ist, sodass der Compiler die weitere Verwendung des jetzt ungültigen Speichers verhindert.

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

Typen mit Destruktoren müssen verschoben werden, wenn sie als Wert verwendet werden (auch bekannt als beim Kopieren von Bytes), da sie eine Ressource verwalten / besitzen (z. B. eine Speicherzuweisung oder ein Dateihandle) und es sehr unwahrscheinlich ist, dass eine Bytekopie diese korrekt dupliziert Eigentum.

"Nun ... was ist eine implizite Kopie?"

Stellen Sie sich einen primitiven Typ vor wie u8: Eine Byte-Kopie ist einfach, kopieren Sie einfach das einzelne Byte, und eine semantische Kopie ist genauso einfach, kopieren Sie das einzelne Byte. Insbesondere wird ein Byte - Kopie ist eine semantische Kopie ... Rust hat sogar ein Einbau-MerkmalCopy , dass Aufnahmen , die Typen identisch semantische und Byte - Kopien haben.

Daher sind CopyBy-Value-Verwendungen für diese Typen auch automatisch semantische Kopien, sodass die Verwendung der Quelle absolut sicher ist.

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

Alt : Der NoCopyMarker überschreibt das automatische Verhalten des Compilers bei der Annahme, dass Typen sein können Copy(dh nur Aggregate von Grundelementen und enthalten &) Copy. Dies wird sich jedoch ändern, wenn integrierte Opt-In-Merkmale implementiert werden.

Wie oben erwähnt, sind integrierte Opt-In-Merkmale implementiert, sodass der Compiler kein automatisches Verhalten mehr aufweist. Die in der Vergangenheit für das automatische Verhalten verwendete Regel entspricht jedoch den Regeln für die Überprüfung, ob die Implementierung zulässig ist Copy.

huon
quelle
@dbaupp: Würdest du zufällig wissen, in welcher Version von Rust die eingebauten Opt-In-Eigenschaften erschienen sind? Ich würde 0,10 denken.
Matthieu M.
@MatthieuM. Es ist noch nicht implementiert, und es wurden kürzlich einige Änderungen am Design der integrierten Opt-Ins vorgeschlagen .
Huon
Ich denke, dass altes Zitat gelöscht werden sollte.
Stargateur
1
# [ableiten (Kopieren, Klonen)] sollte auf Triplet verwendet werden, nicht impl
shadowbq
6

Am einfachsten ist es, etwas in Ihren Typ einzubetten, das nicht kopierbar ist.

Die Standardbibliothek bietet einen " Markertyp " für genau diesen Anwendungsfall: NoCopy . Beispielsweise:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}
BurntSushi5
quelle
15
Dies gilt nicht für Rust> = 1.0.
Malbarbo