Ownership

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:

  1. Always $O(1)$ shallow stack copy: Even if s1 owns 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.
  2. 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 marks s1 as uninitialized at compile time. Neither your code nor Drop ever touches s1 again.
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:

  1. You cannot read the variable after the if statement, because it is possibly moved (E0382).
  2. 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