How move semantics work in Rust
Short answer: a move shifts the value’s bytes to a new place and stops counting the old place as
initialised. Both of those are compile-time decisions. Whatever the value points at lives elsewhere and
stays where it is, so moving a String copies three words no matter how long the text is.
The heap buffer doesn’t move
Section titled “The heap buffer doesn’t move”Note where a String’s text lives, then move the String:
fn main() { let original = String::from("hello"); let buffer = original.as_ptr();
let moved = original;
println!("same buffer: {}", buffer == moved.as_ptr()); println!("bytes copied: {}", std::mem::size_of_val(&moved));}same buffer: truebytes copied: 24as_ptr returns the address of the heap buffer, and both bindings report the same one after the move. The
five bytes of text weren’t copied anywhere. What changed location is the String value itself — a pointer,
a length and a capacity, the same three words from
why Rust moves values. size_of_val puts a number on it: 24 bytes
on a 64-bit machine.
That number is the type’s, not the text’s, so a long string is no dearer to move than a short one:
fn main() { let short = String::from("hi"); let long = "x".repeat(100_000);
println!("short: {} bytes", std::mem::size_of_val(&short)); println!("long: {} bytes", std::mem::size_of_val(&long)); println!("long is {} characters", long.len());}short: 24 byteslong: 24 byteslong is 100000 charactersMoving into a function doesn’t change any of this. A parameter is another binding, and the buffer address on the far side of the call is the one you started with.
When the bytes are the payload
Section titled “When the bytes are the payload”A String keeps its text on the heap, which is why its move is three words however long the text is. Put
the payload inside the value instead and all of it travels:
struct Blob { data: [u8; 1024], tag: String,}
fn main() { let blob = Blob { data: [7; 1024], tag: String::from("payload"), };
println!("Blob is {} bytes", std::mem::size_of::<Blob>());
let moved = blob;
println!("last data byte: {}", moved.data[1023]); println!("tag: {}", moved.tag);}Blob is 1048 byteslast data byte: 7tag: payload1024 of those bytes are the array and 24 are the String, and the move relocates all 1048: the array turns
up under the new name at a different address, with its contents intact. That’s why size_of::<Blob>() is
the price of moving one. ptr::read describes the copy in
one line — it “creates a bitwise copy of T, regardless of whether T is Copy” — and a move is that copy
with the source retired afterwards. Most types are a handful of words, so this rarely matters. When it does,
the fix is to keep the payload behind a pointer, which is what String, Vec and Box all do.
What is left behind
Section titled “What is left behind”The old binding isn’t zeroed and isn’t freed. The compiler stops treating it as initialised and refuses to read it, which is the state the previous article works through. The bytes at that spot didn’t change. What changed is what the compiler lets you do with them.
Related
Section titled “Related”- How to tell whether a type will move or copy — this page’s predecessor, and how to test which one you’ll get
- What happens to the original variable after a move? — the moved-from binding, in detail
- Moves and ownership — the rest of this chapter
- Next in this chapter: why you can’t use a value after moving it