是否有可能使一个类型只能移动而不能复制?

编者按 : 这个问题是在 Rust 1.0之前提出的,问题中的一些断言在 Rust 1.0中不一定是正确的。一些答案已经更新,以解决这两个版本。

我有这个结构

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

如果我把它传递给一个函数,它会被隐式复制。现在,有时我读到一些值是不可复制的,因此必须移动。

有没有可能使这个结构 Triplet不可复制?例如,有没有可能实现一个 trait,使得 Triplet不可复制,因此是“可移动的”?

我在哪里读到过,一个人必须实现 Clone trait 来复制不能隐式复制的东西,但我从来没有读到过相反的情况,那就是拥有一个隐式可复制的东西,并使它不能隐式复制,所以它会移动。

这说得通吗?

23687 次浏览

The easiest way is to embed something in your type that is not copyable.

The standard library provides a "marker type" for exactly this use case: NoCopy. For example:

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

Preface: This answer was written before opt-in built-in traits—specifically the Copy aspects—were implemented. I've used block quotes to indicate the sections that only applied to the old scheme (the one that applied when the question was asked).


Old: To answer the basic question, you can add a marker field storing a NoCopy value. E.g.

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

You can also do it by having a destructor (via implementing the Drop trait), but using the marker types is preferred if the destructor is doing nothing.

Types now move by default, that is, when you define a new type it doesn't implement Copy unless you explicitly implement it for your type:

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

The implementation can only exist if every type contained in the new struct or enum is itself Copy. If not, the compiler will print an error message. It can also only exist if the type doesn't have a Drop implementation.


To answer the question you didn't ask... "what's up with moves and copy?":

Firstly I'll define two different "copies":

  • a byte copy, which is just shallowly copying an object byte-by-byte, not following pointers, e.g. if you have (&usize, u64), it is 16 bytes on a 64-bit computer, and a shallow copy would be taking those 16 bytes and replicating their value in some other 16-byte chunk of memory, without touching the usize at the other end of the &. That is, it's equivalent to calling memcpy.
  • a semantic copy, duplicating a value to create a new (somewhat) independent instance that can be safely used separately to the old one. E.g. a semantic copy of an Rc<T> involves just increasing the reference count, and a semantic copy of a Vec<T> involves creating a new allocation, and then semantically copying each stored element from the old to the new. These can be deep copies (e.g. Vec<T>) or shallow (e.g. Rc<T> doesn't touch the stored T), Clone is loosely defined as the smallest amount of work required to semantically copy a value of type T from inside a &T to T.

Rust is like C, every by-value use of a value is a byte copy:

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


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


foo(y) // byte copy

They are byte copies whether or not T moves or is "implicitly copyable". (To be clear, they aren't necessarily literally byte-by-byte copies at run-time: the compiler is free to optimise the copies out if code's behaviour is preserved.)

However, there's a fundamental problem with byte copies: you end up with duplicated values in memory, which can be very bad if they have destructors, e.g.

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

If w was just a plain byte copy of v then there would be two vectors pointing at the same allocation, both with destructors that free it... causing a double free, which is a problem. NB. This would be perfectly fine, if we did a semantic copy of v into w, since then w would be its own independent Vec<u8> and destructors wouldn't be trampling on each other.

There's a few possible fixes here:

  • Let the programmer handle it, like C. (there's no destructors in C, so it's not as bad... you just get left with memory leaks instead. :P )
  • Perform a semantic copy implicitly, so that w has its own allocation, like C++ with its copy constructors.
  • Regard by-value uses as a transfer of ownership, so that v can no longer be used and doesn't have its destructor run.

The last is what Rust does: a move is just a by-value use where the source is statically invalidated, so the compiler prevents further use of the now-invalid memory.

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

Types that have destructors must move when used by-value (aka when byte copied), since they have management/ownership of some resource (e.g. a memory allocation, or a file handle) and its very unlikely that a byte copy will correctly duplicate this ownership.

"Well... what's an implicit copy?"

Think about a primitive type like u8: a byte copy is simple, just copy the single byte, and a semantic copy is just as simple, copy the single byte. In particular, a byte copy is a semantic copy... Rust even has a built-in trait Copy that captures which types have identical semantic and byte copies.

Hence, for these Copy types by-value uses are automatically semantic copies too, and so it's perfectly safe to continue using the source.

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

Old: The NoCopy marker overrides the compiler's automatic behaviour of assuming that types which can be Copy (i.e. only containing aggregates of primitives and &) are Copy. However this will be changing when opt-in built-in traits is implemented.

As mentioned above, opt-in built-in traits are implemented, so the compiler no longer has automatic behaviour. However, the rule used for the automatic behaviour in the past are the same rules for checking whether it is legal to implement Copy.