8.4Moves: assignment transfers ownership
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:
borrow of moved value:s1``: the headline. A moved value is whats1became on line 3. ("Borrow" is chapter 9's word; for today, read it as "use." Usings1after its value moved is the crime.)move occurs becauses1has typeString, which does not implement theCopytrait: the compiler explaining why assignment moved rather than copied. Some types get copied on=after all, markedCopy;Stringis not one of them, for exactly the double-free reason above. The whole next lesson unpacks this line ("trait" itself is a chapter 16 word; the compiler is just naming the marker).value moved herepointing atlet s2 = s1;: the move itself, located to the character.value borrowed here after movepointing at{s1}: the illegal use, located just as precisely.- The help: the compiler offers a legal alternative, in diff form, the same way the typo-fixers of chapter 3 did.
.clone()is lesson 8.7's subject, and yes, it's option two from the thought experiment, made explicit and visible. The compiler is reading the chapter ahead of you.
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.