8.4Moves: assignment transfers ownership

Last updated June 12, 2026

This is the lesson the chapter is named for. It's short on purpose; the idea inside is the single most important one in the course, and it deserves a page with no crowds.

A thought experiment

Take the diagram from lesson 8.2: a String is a fixed-size handle in a stack frame (address, length, capacity) pointing at a heap block holding the text. Now ask what this innocent line should do:

let s1 = String::from("hello");
let s2 = s1;

For an i32, chapter 1 settled it: the value is copied into the new box, and you have two independent fives. Try to apply that to the String, and you must pick which part gets copied.

Option one: copy the handle. s2 gets its own copy of the address, length, and capacity. Cheap! Also catastrophic. Both handles now point at the same heap block, and lesson 8.3 just taught what happens at the closing brace: each owner drops its value. s2 is dropped, returning the block. Then s1 is dropped, returning the same block again. That's a double free, one of lesson 8.1's three villains, manufactured by an assignment statement. (This is precisely the bug C++ programmers learn to fear and hand-patch; learncpp devotes a chunk of its chapter 22 to building a class that makes this mistake and then repairing it.)

Option two: copy the heap block too. Ask the allocator for a fresh block, duplicate the text, give s2 a handle to the copy. Safe! But now = quietly costs whatever the text costs. Copy a ten-megabyte String with an innocent-looking assignment, and you've spent real time and memory without one visible signal in the code. Rust's designers found "the cheap-looking thing is secretly expensive" as distasteful as the double free.

So copying the handle is unsafe, and copying the block is dishonest. Rust picks neither.

Rust's answer: the move

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s2}");
}
hello

The handle is copied to s2, the heap block stays where it is, and then the crucial step: s1 is dead. Not "shares the block," not "is a backup." The value moved from s1 to s2, and s2 is now its one owner. Rule 2 from lesson 8.1, one owner at a time, in action. There is exactly one live handle, so the closing brace drops the block exactly once, and the double free is structurally impossible.

This is why the course has been saying Rust teaches moves as "how assignment works." In C++, transferring ownership like this is an advanced technique with dedicated syntax, taught (in learncpp's case) fourteen chapters after assignment. In Rust it is assignment, whenever the type owns heap memory.

But a claim like "s1 is dead" should make you suspicious. Its name is still in scope; nothing from chapter 2 stops the next line from saying println!("{s1}"). What happens if it does?

The error message is the textbook

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s1}");
}
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1}");
  |                ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

Lesson 1.7 taught you to read these top to bottom, and this one is the most famous diagnostic in Rust. Line by line:

Take a second to appreciate what this error is. In C++, the use-after-move bug compiles, runs, and corrupts. Here it's a four-line explanation with the cause, the location, and a suggested fix. The error message is the textbook, and you'll meet this particular page many times; every Rust programmer does.

Moved is not out of scope

After let s2 = s1;, the name s1 is still in scope (no E0425 here). What's gone is its value. Keep last lesson's distinction sharp: scope is about names and braces; moves are about values and ownership. A variable can stand there, perfectly named, owning nothing.

Moves happen wherever values change hands

Assignment between two lets is just the cleanest specimen. Two more habits to install now:

Reassignment drops the old value. If a mut variable holding a String is assigned a new one, the old value has no owner left, so it's dropped on the spot, not at the brace:

fn main() {
    let mut motto = String::from("move fast");
    println!("{motto}");
    motto = String::from("move carefully");
    println!("{motto}");
}
move fast
move carefully

The "move fast" block is returned at line 4, the moment its last owner stopped owning it; it doesn't linger until the brace, because by then nobody owns it.

Shadowing is not moving. Don't confuse this chapter with lesson 5.2. let s = s.trim() and friends create a new variable that hides the old name; a move ends a value. Shadowing is about names; moving is about values. (When you shadow a String with something built from it, a move may also be involved, but the two concepts stay distinct; the quiz pokes at this.)

Quiz time

Question #1

Compile or not? One sentence of why.

fn main() {
    let a = String::from("first");
    let b = a;
    let c = b;
    println!("{c}");
}
Show solution

Compiles, and prints first. The value moves from a to b to c, a relay with one baton; at every moment there's exactly one owner, and only the current owner (c) is used. Both braces-worth of cleanup amount to one drop, by c.

Question #2

Compile or not? One sentence of why.

fn main() {
    let a = String::from("first");
    {
        let b = a;
        println!("{b}");
    }
    println!("{a}");
}
Show solution

error[E0382]: borrow of moved value: a``. The value moves from a into the block's b and is dropped at the block's closing brace (b owns it, and b's scope ends there). The final line then uses a, whose value moved away and no longer exists anywhere. Note that a's name is still in scope; that's not the problem. The value is gone.

Question #3

After these two lines, describe the memory picture in lesson 8.2's terms: how many handles, how many heap blocks, and who gets dropped at the brace?

let recipe = String::from("stir twice");
let backup = recipe;
Show solution

One heap block (holding stir twice) and one live handle: backup's. The boxes for recipe's handle still physically sit in the frame, but the compiler has marked recipe as moved-from, and no code is allowed to look at it. At the closing brace, backup is dropped and returns the block; recipe drops nothing, because it owns nothing. Exactly one give-back for exactly one block.

The error message said it: String "does not implement the Copy trait." Which types do, why integers have been serenely ignoring this entire chapter, and what = means for them is next lesson's story.