The short answer is it depends, and it depends mainly on the compiler optimizations:
machine code wise, there is, at first, no difference between a
Copy and a non-
Copy value. Indeed, the true meaning of
Copy is: "the compiler won't invalidate / prevent you from using a "moved" value". And "moving" a value is, by default, achieved by bitwise-copying the value (
memmove), in all cases (in the
!Copy case, the compiler will just forbid you from using the initial data / memory, which has become stale).
Now, in practice, this is actually very hard to predict, since both Rust and its compiler backend (LLVM) will obviously try to optimize "unnecessary bit-copies".
A good example of the latter, and completely on point for this thread, is the following quote:
Indeed, in that example we can see that a
Vec, which is three-pointers wide, is considered wide enough for it to be "passed
by reference with indirection" even when moved (I think this can happen thanks to the undefined (
") ABI for functions).
So, ownership is used to express who drops a value, and
Copy to express wether a moved-out-of value gets invalidated / becomes unusable or not.
Anything machine-related, on the other hand (e.g., whether to perform a bitwise copy or to use indirection), is left for the compiler to decide, and is thus quite hard to predict.
on the other hand, there are some cases that guarantee that a bitwise copy cannot be happening: when using types that do bundle indirection within them, such as:
ref-counted references types (
Arc (or its single-threaded optimization,
or when using compile-time-checked short-lived borrows (shared,
&, or exclusive,
But, imho, if an element is big enough for implicit compiler-inserted bitwise copies to be an issue (even when moving a value around), then it is not for the user of the type to start using, for instance, a
Box around it, but rather the one defining the type to bundle an internal
Box-like indirection to improve that.
One good example of that is when dealing with
enums, and when there is one variant that may be quite bigger than then others. In that case,
Box-ing that variant will mitigate the cost of moving around such an
enum, even in the case of other variants being used.
One very frequent case of that are
Error types: those get bundled as the
Err branch within the
enum, which is one branch which is not expected to be hit that much, in theory (failure path).
And yet, if the
Error type is big, every time a function returning a
Result with that
Error type succeeds, the compiler may be emitting a big bitwise copy since the size of the
enum itself needs to be able to hold the
So, in that case, a good strategy is to
Box the data / payload / info bundled within the
Error variant, so as to have an upper-bound on the size of that variant (1-pointer-wide in the case of a
Box-ed fixed type, and two in the case of a "fat pointer", that is, a pointer to a
!Sized type such as
- Since 2-pointer-wide is already quite big for some small return types in the
Box<dyn Error> is already a bit suboptimal, despite it being used in many places. In order to palliate that, one can double-box
Box<Box<dyn Error>>, so as to make sure the
Error variant is not more than 1-pointer-wide, or use special layout-optimized type-erased
Error types, such as