Move Semantics
Move semantics is Rust's default behavior when assigning, passing, or returning any value whose type does not implement the Copy trait (such as String, Vec<T>, Box<T>, File, or custom structs). A move transfers ownership of the value from the source binding to the destination binding by performing a shallow bitwise copy of the stack header and statically invalidating the source binding so it can never be used or dropped again.
What Happens in Memory During a Move
Consider moving a String from s1 to s2 with let s2 = s1;. A String consists of a 24-byte stack header (ptr, len, cap) pointing to a UTF-8 byte buffer on the heap:
Before Move (`let s1 = String::from("rust");`):
Stack Heap
+------------------+ +---+---+---+---+
| s1 (Owner) | | r | u | s | t |
| ptr ───────────+───────────────> +---+---+---+---+
| len = 4, cap=4 |
+------------------+
After Move (`let s2 = s1;`):
Stack Heap
+------------------+
| s1 (INVALIDATED) | (Compiler forbids reading OR dropping `s1`!)
+------------------+ +---+---+---+---+
| s2 (New Owner) | | r | u | s | t |
| ptr ───────────+───────────────> +---+---+---+---+
| len = 4, cap=4 | (Heap bytes are NOT copied!)
+------------------+
Notice two crucial properties of a Rust move:
- Always $O(1)$ shallow stack copy: Even if
s1owns a 1 GB heap buffer,let s2 = s1;copies only the 24-byte(ptr, len, cap)struct on the stack. Rust never implicitly deep-copies heap data. - Destructive move (Compile-time invalidation): Unlike C++ (
std::move), which leaves the source object in a "valid but unspecified" state whose destructor still runs at scope exit, Rust markss1as uninitialized at compile time. Neither your code norDropever touchess1again.
fn main() {
let s1 = String::from("zero-copy ownership transfer");
// Record the heap buffer address before moving:
let addr_before = s1.as_ptr();
// Move `s1` into `s2`:
let s2 = s1;
let addr_after = s2.as_ptr();
// The heap address is identical—not a single heap byte was copied!
assert_eq!(addr_before, addr_after);
println!("s2 = '{s2}' (heap buffer address: {addr_after:p})");
}
s2 = 'zero-copy ownership transfer' (heap buffer address: 0x600001718040)
Five Operations That Trigger a Move
For any non-Copy type, ownership is moved in five everyday situations:
| Operation | Example | Where Ownership Moves |
|---|---|---|
| 1. Variable assignment | let b = a; |
From a into b |
| 2. Passing by value to a function | process(a); |
From a into process's parameter |
| 3. Returning by value from a function | return a; / tail a |
From local a into the caller's receiving slot |
| 4. Constructing a struct, tuple, enum, or collection | let v = vec![a]; or Some(a) |
From a into the container/variant |
| 5. Pattern matching by value | if let Some(inner) = opt |
From opt into inner |
struct Envelope {
payload: String,
}
fn wrap_in_envelope(text: String) -> Envelope {
// Moves `text` into the `payload` field of `Envelope`, and returns `Envelope` to caller:
Envelope { payload: text }
}
fn main() {
let raw = String::from("important message");
// 1. `raw` moves into `wrap_in_envelope` parameter
// 2. Returned `Envelope` moves into `env`
let env = wrap_in_envelope(raw);
// 3. Move `env.payload` out of the struct into `extracted`:
let extracted = env.payload;
println!("Extracted payload: {extracted}");
}
Extracted payload: important message
Reinitializing a Moved Variable
Even though moving out of a variable x leaves x uninitialized, if x was declared with let mut x, you can assign a brand-new value to x and continue using it! Because x is already empty (moved out of), reassigning x = new_value; does not need to drop anything first:
fn consume(s: String) {
println!("Consumed: {s}");
}
fn main() {
let mut current = String::from("first value");
consume(current); // `current` is moved out (now uninitialized)
// Reinitialize `current` with a new String:
current = String::from("second value");
println!("Reinitialized current: {current}");
}
Consumed: first value
Reinitialized current: second value
Conditional Moves and Drop Flags
If a variable is moved inside one branch of an if statement (and not moved in the else branch), two rules apply:
- You cannot read the variable after the
ifstatement, because it is possibly moved (E0382). - At the end of the function, the compiler uses a hidden stack drop flag to drop the variable only if the branch that did not move it was taken at runtime!
// ❌ does not compile
fn main() {
let token = String::from("abc-123");
let send_now = true;
if send_now {
let _sent = token; // Moved on this branch only
}
println!("Still trying to read token: {token}");
}
error[E0382]: borrow of moved value: `token`
--> src/main.rs:9:43
|
2 | let token = String::from("abc-123");
| ----- move occurs because `token` has type `String`, which does not implement the `Copy` trait
...
6 | let _sent = token; // Moved on this branch only
| ----- value moved here
...
9 | println!("Still trying to read token: {token}");
| ^^^^^^^ value borrowed here after move
error: aborting due to 1 previous error