¿Es posible hacer que un tipo sea solo movible y no copiable?

96

Nota del editor : esta pregunta se hizo antes de Rust 1.0 y algunas de las afirmaciones en la pregunta no son necesariamente ciertas en Rust 1.0. Algunas respuestas se han actualizado para abordar ambas versiones.

Tengo esta estructura

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

Si paso esto a una función, se copia implícitamente. Ahora, a veces leo que algunos valores no se pueden copiar y, por lo tanto, deben moverse.

¿Sería posible hacer que esta estructura Tripletno se pueda copiar? Por ejemplo, ¿sería posible implementar un rasgo que lo hiciera Tripletno copiable y por lo tanto "movible"?

Leí en alguna parte que uno tiene que implementar el Clonerasgo para copiar cosas que no se pueden copiar implícitamente, pero nunca leí al revés, es decir, tener algo que se puede copiar implícitamente y hacerlo no copiable para que se mueva en su lugar.

¿Eso tiene algún sentido?

Christoph
fuente
1
paulkoerbitz.de/posts/… . Aquí hay buenas explicaciones de por qué moverse frente a copiar.
Sean Perry

Respuestas:

164

Prefacio : esta respuesta se escribió antes de que se implementaran los rasgos incorporados opcionales, específicamente los Copyaspectos . He usado comillas en bloque para indicar las secciones que solo se aplicaban al esquema anterior (el que se aplicaba cuando se hizo la pregunta).


Antiguo : para responder a la pregunta básica, puede agregar un campo de marcador que almacene un NoCopyvalor . P.ej

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

También puede hacerlo teniendo un destructor (mediante la implementación del Droprasgo ), pero se prefiere usar los tipos de marcador si el destructor no está haciendo nada.

Los tipos ahora se mueven de forma predeterminada, es decir, cuando define un nuevo tipo, no se implementa a Copymenos que lo implemente explícitamente para su tipo:

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

La implementación solo puede existir si todos los tipos contenidos en el nuevo structo enumson ellos mismos Copy. De lo contrario, el compilador imprimirá un mensaje de error. También puede existir solo si el tipo no tiene una Dropimplementación.


Para responder a la pregunta que no hiciste ... "¿qué pasa con los movimientos y la copia?":

En primer lugar, definiré dos "copias" diferentes:

  • una copia de bytes , que es simplemente copiar superficialmente un objeto byte por byte, sin seguir punteros, por ejemplo, si tiene (&usize, u64), son 16 bytes en una computadora de 64 bits, y una copia superficial sería tomar esos 16 bytes y replicar sus valor en algún otro bloque de memoria de 16 bytes, sin tocar el usizeen el otro extremo del &. Es decir, equivale a llamar memcpy.
  • una copia semántica , duplicando un valor para crear una nueva instancia (algo) independiente que se puede usar de forma segura por separado de la anterior. Por ejemplo, una copia semántica de a Rc<T>implica simplemente aumentar el recuento de referencias, y una copia semántica de a Vec<T>implica crear una nueva asignación y luego copiar semánticamente cada elemento almacenado del antiguo al nuevo. Pueden ser copias profundas (p Vec<T>. Ej. ) O superficiales (p. Ej. Rc<T>, No toca lo almacenado T), Clonese define vagamente como la menor cantidad de trabajo necesaria para copiar semánticamente un valor de tipo Tdesde dentro de &Ta T.

Rust es como C, cada uso por valor de un valor es una copia de byte:

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

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

foo(y) // byte copy

Son copias de bytes ya sea que T mueven como son "copiables implícitamente". (Para ser claros, no son necesariamente copias literalmente byte por byte en tiempo de ejecución: el compilador es libre de optimizar las copias si se conserva el comportamiento del código).

Sin embargo, hay un problema fundamental con las copias de bytes: terminas con valores duplicados en la memoria, lo que puede ser muy malo si tienen destructores, p. Ej.

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

Si wfuera solo una copia de byte simple, vhabría dos vectores apuntando a la misma asignación, ambos con destructores que lo liberan ... causando un doble libre , lo cual es un problema. NÓTESE BIEN. Esto estaría perfectamente bien, si hiciéramos una copia semántica de vinto w, ya que entonces wserían independientes Vec<u8>y los destructores no se pisotearían entre sí.

Aquí hay algunas posibles correcciones:

  • Deje que el programador lo maneje, como C. (no hay destructores en C, por lo que no es tan malo ... simplemente se queda con pérdidas de memoria.: P)
  • Realiza una copia semántica implícitamente, de modo que wtenga su propia asignación, como C ++ con sus constructores de copia.
  • Considere los usos por valor como una transferencia de propiedad, por lo que vya no se puede usar y no se ejecuta su destructor.

Lo último es lo que hace Rust: un movimiento es solo un uso por valor donde la fuente está invalidada estáticamente, por lo que el compilador evita el uso posterior de la memoria ahora no válida.

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

Los tipos que tienen destructores deben moverse cuando se usan por valor (también conocido como cuando se copian bytes), ya que tienen administración / propiedad de algún recurso (por ejemplo, una asignación de memoria o un identificador de archivo) y es muy poco probable que una copia de bytes duplique correctamente esto propiedad.

"Bueno ... ¿qué es una copia implícita?"

Piense en un tipo primitivo como u8: una copia de byte es simple, simplemente copie el byte único, y una copia semántica es igual de simple, copie el byte único. En particular, una copia de bytes es una copia semántica ... Rust incluso tiene un rasgo incorporadoCopy que captura qué tipos tienen copias semánticas y de bytes idénticas.

Por lo tanto, para estos Copytipos, los usos por valor también son copias semánticas automáticamente, por lo que es perfectamente seguro continuar usando la fuente.

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

Antiguo : el NoCopymarcador anula el comportamiento automático del compilador de asumir que los tipos que pueden ser Copy(es decir, que solo contienen agregados de primitivas y &) lo son Copy. Sin embargo, esto cambiará cuando se implementen los rasgos incorporados opcionales .

Como se mencionó anteriormente, se implementan rasgos incorporados opt-in, por lo que el compilador ya no tiene comportamiento automático. Sin embargo, la regla utilizada para el comportamiento automático en el pasado son las mismas reglas para verificar si su implementación es legal Copy.

huon
fuente
@dbaupp: ¿Sabría por casualidad en qué versión de Rust aparecieron los rasgos incorporados opt-in? Creo que 0,10.
Matthieu M.
@MatthieuM. aún no se ha implementado y, de hecho, recientemente se han propuesto algunas revisiones al diseño de las incorporaciones opcionales .
huon
Creo que esa vieja cita debería borrarse.
Stargateur
1
# [derive (Copy, Clone)] debe usarse en Triplet no
impl
6

La forma más sencilla es incrustar algo en su tipo que no se pueda copiar.

La biblioteca estándar proporciona un "tipo de marcador" exactamente para este caso de uso: NoCopy . Por ejemplo:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}
BurntSushi5
fuente
15
Esto no es válido para Rust> = 1.0.
malbarbo