Implementation-wise duplicating a Copy
type or moving a non-Copy
type is the same basic operation: it’s just copying the shallow data of that value (i.e. it’s not a deep copy, everything behind pointer-indirection remains untouched).
The main difference is that non-Copy
types typically cannot be duplicated, so the original value where you moved from needs to become invalidated. This is important for Rust’s ownership sytem: say…
…for example… a Box<u8>
owns a heap allocation, and if it was duplicated, then there’d be two boxes owning the same heap allocation, which can’t happen (because then, dropping both of them would result in a double-free).
The compiler makes sure that always only one copy of a (non-Copy
) value is considered live at each point in time; this is done mostly with static checks when you’re moving values around between variables on the stack, though it’s possible to conditionally move out of a value, and in such cases the compiler might introduce additional run-time flags on the stack to track whether or not a value is still valid, which is important to decide whether or not it needs to be dropped at the end of the scope.
You can actually take the stance that these flags as, in principle, always being involved when moving from or into values on the stack, just they’re (very) often optimized away. With these flags, the model of moving a value from, say, variable x
to variable y
with an assignment operation y = x
consists of three steps:
- first, if the target variable
y
already contains a valid value (as determined by its run-time flag) then this old value is dropped (by calling its destructor if necessary)
- then, the (shallow) data of
x
is copied to y
- finally, the run-time flags for both
x
and y
are adjusted to indicate that y
contains a valid value, and x
no longer contains a valid value
There’s no way to inspect these flags though, they only control destructors, and the compiler will outright prevent any other (beyond the implicit destruction at the end of its scope) usage of a variable unless it’s 100% certain (via static analysis) that at that point in the program the variable contains a valid value.
If you want to move values around with run-time checks, you can do it with Option<T>
, using API such as Option::take
to – essentially – de-initialize the old value, without making preventing subsequent inspection of it (since it’s new state is simply that it’s taking on a None
value; serving a similar purpose as what the “drop flags” did in the previous paragraphs), and if you want to set a value you can e.g. assign Some(…value…)
. The example of Option::take
leads naturally into a few standard-library-implemented moving primitives,
feel free to take a look. Those can move a value out from behind a &mut …
reference. Since such a reference could be pointing anywhere, including on the heap, and/or in a complex data structure, there are no runtime flags anymore, and you’ll need to ensure more predictable behavior: the value can never become invalid, so you’ll need to provide an immediate replacement value (or let it use Default::default()
, depending on which one of those functions you use). The fact that drop flags aren’t involved also simplifies these operations: E.g. for mem::swap
the swapping really is just nothing more beyond copying/swapping the data between the two values.
The concept of “moving” in Rust is not just about moving with language-built-in operations such as assignment, or with the functions above; any custom (low level, unsafe
-code involving) library can do what’s logically a “move” just by copying data behind pointers. Just gotta make sure you don’t ever accidentally duplicate any value this way. And even these standard library methods do it this way; take a look at the implementation of mem::replace
, it’s really quite straightforward code.
use std::ptr;
pub fn replace<T>(dest: &mut T, src: T) -> T {
// SAFETY: We read from `dest` but directly write `src` into it afterwards,
// such that the old value is not duplicated. Nothing is dropped and
// nothing here can panic.
unsafe {
let result = ptr::read(dest);
ptr::write(dest, src);
result
}
}
The term “moving” can also refer more generally to “transferring ownership”, in which case things can “move” without actually moving in memory. So e.g. some people might consider the contents of type T
to be “moving”, too, when you’re moving a Box<T>
; because the (transfer of) ownership of the T
comes with the (transfer of) ownership of the Box<T>
.
Finally, looking back at Copy
types, these types are allowed to be duplicated, so this means things like
- the compiler’s static analysis will no longer prevent you from using a value that you did (or might have) moved from
- you don’t need functions like
mem::replace
anymore to move out of a &mut T
, because it’s safe to skip the part of immediately providing a replacement value; so dereferencing, say, a reference: &mut u32
into a u32
just works like let x: u32 = *reference;
- copying is the norm for
Copy
types; there’s no disadvantage because it’s the same cost as ordinary moving (potentially even cheaper because you never need to deal with run-time flags for destructors)
- this means it’s really pretty hard to “actually move” a
Copy
type instead of copying it, but also that doesn’t matter because moving would do the same thing, just with more restrictions
- one point however is destructors: If a
Copy
type had a destructor, then surely all those implicit duplications might become confusing or annoying, because for each copy, there’d be a destructor call? Right. That’s why Copy
types aren’t allowed to have any custom destructors; dropping a Copy
type is always a no-op.