Why this matters
The previous lesson ended with a question: who frees each heap block, and when? Rust's answer is ownership, three short rules enforced by the compiler. They make memory management automatic and free of garbage collection, and they eliminate double frees and use-after-free bugs. They also produce the error every Rust learner meets first and most often, E0382 "use of moved value". After this lesson you will know exactly what that error protects you from.
The three rules
- Each value has an owner, a variable (or a field of something, or an element of a collection).
- There is exactly one owner at a time.
- When the owner goes out of scope, the value is dropped: its resources, such as a heap buffer, are freed.
Rule 3 is the automatic cleanup:
fn main() {
{
let s = String::from("temporary");
println!("{s}");
} // s goes out of scope here: its heap buffer is freed
println!("s is gone");
}
temporary
s is gone
At the closing brace, the compiler inserts a call that frees s's buffer. You never write free, and you cannot forget it. (Lesson 4 looks closely at this "drop".) The interesting consequences come from rule 2.
What assignment does to a String
Consider:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s2}");
}
What does let s2 = s1; do? There are three possibilities, and it helps to see why Rust picks the one it does.
Option A: copy the handle only (a "shallow copy"). The 24 bytes on the stack are duplicated, so both point at the same heap buffer:
s1 | ptr ----------+
| len 5 | +---+---+---+---+---+
| cap 5 +-----> | h | e | l | l | o |
| +---+---+---+---+---+
s2 | ptr ----------+
| len 5
| cap 5
Now both s1 and s2 would free the buffer when they go out of scope: a double free. And if one were changed (grown, reallocated), the other would point at freed memory.
Option B: copy the heap data too (a "deep copy"). Safe, but potentially very expensive, and hidden in an innocent-looking =. Rust never does this implicitly. (You can ask for it explicitly with clone; next lesson.)
Option C, Rust's choice: copy the handle, then consider s1 invalid. This is a move:
s1 | (moved: unusable)
|
| +---+---+---+---+---+
s2 | ptr ----------------> | h | e | l | l | o |
| len 5 +---+---+---+---+---+
| cap 5
The 24 bytes are copied (cheap, like any small value), ownership of the buffer transfers to s2, and the compiler forbids any further use of s1. There is always exactly one owner, so the buffer is freed exactly once, when s2 goes out of scope. The move costs nothing at run time beyond copying 24 bytes; the invalidation exists only in the compiler's analysis.
E0382: use of moved value
Try to use s1 after the move:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world");
println!("{s2}");
}
error[E0382]: borrow of moved value: `s1`
--> main.rs:4:15
|
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}, world");
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
Read it carefully, because it tells the whole story:
move occurs because s1 has type String, which does not implement the Copy trait: Strings move; some simpler types are copied instead (next lesson).value moved here: the line where ownership lefts1.value borrowed here after move: the line that tried to use it. (println!borrows its arguments, hence "borrowed"; module 5.)- The help offers
clone(), which would make a deep copy. Sometimes that is the right fix. Often a better one is to reorder the code, or to borrow instead of moving (module 5).
Notice what this error is preventing. In C, the equivalent code compiles, and if both variables freed the buffer you would have a double free, or if one freed it you would have a use-after-free. Rust turns that into a compile-time error with arrows pointing at the cause.
Moves happen in more places than let
Ownership moves whenever a value is transferred:
- assigning it:
let b = a; - passing it to a function:
takes(a);(lesson 5) - returning it from a function
- putting it into a collection or struct:
v.push(a); - assigning it to an existing variable:
longest = a;
The same rule applies everywhere: after the move, the source cannot be used.
fn main() {
let mut names = Vec::new();
let n = String::from("Ada");
names.push(n);
println!("{:?}", names);
}
["Ada"]
names.push(n) moved the String into the vector, which now owns it. Using n after that line would be E0382.
Moves in loops and branches
The compiler tracks moves along every path the program might take. A move inside a loop is a problem if the loop can come around and use the value again:
fn main() {
let label = String::from("item");
let mut all = Vec::new();
for _ in 0..3 {
all.push(label);
}
}
error[E0382]: use of moved value: `label`
--> main.rs:5:18
|
2 | let label = String::from("item");
| ----- move occurs because `label` has type `String`, which does not implement the `Copy` trait
3 | let mut all = Vec::new();
4 | for _ in 0..3 {
| ------------- inside of this loop
5 | all.push(label);
| ^^^^^ value moved here, in previous iteration of loop
|
help: consider moving the expression out of the loop so it is only moved once
|
4 ~ let mut value = all.push(label);
5 ~ for _ in 0..3 {
6 ~ value;
|
help: consider cloning the value if the performance cost is acceptable
|
5 | all.push(label.clone());
| ++++++++
The first pass moves label into the vector; the second pass would use a moved value. The message says "value moved here, in previous iteration of loop". The first help: is a good example of a suggestion to ignore: it would move the whole push out of the loop, which is not what the program means. The real fixes: create a new String inside the loop, or push a clone (the second suggestion).
Moves in branches are tracked too. If a value is moved in only one branch of an if, the compiler treats it as possibly moved afterwards and forbids using it:
fn main() {
let s = String::from("maybe");
let keep = std::env::args().count() > 5;
if keep {
let owner = s;
println!("kept {owner}");
}
println!("{s}");
}
error[E0382]: borrow of moved value: `s`
--> main.rs:8:15
|
2 | let s = String::from("maybe");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
...
5 | let owner = s;
| - value moved here
...
8 | println!("{s}");
| ^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
5 | let owner = s.clone();
| ++++++++
The compiler cannot know whether keep will be true at run time, so it takes the safe view.
Giving a moved-from variable a new value
A moved-from variable is not dead forever. If it is mut, you can assign a fresh value, and it becomes valid again:
fn main() {
let mut current = String::from("first");
let old = current;
current = String::from("second");
println!("{old} then {current}");
}
first then second
Assigning to a variable that still holds a value drops the old value first, so current = ... on a valid variable frees its previous buffer automatically. No leaks.
Common mistakes
Using a value after passing it on. Typical in "debug print after the fact" code, as in the exercise: move the print before the move, or (after module 5) borrow.
Reaching for clone reflexively. It silences E0382 but copies the heap data, sometimes in a hot loop. Ask first: does the code really need two independent copies? Often reordering or borrowing is right.
Expecting integers to move. let y = x; with an i32 leaves x usable. That is not an exception to ownership but a different category of type, Copy, covered next.
Recap
- Every value has exactly one owner; when the owner goes out of scope, the value is dropped and its heap memory freed.
- Assigning, passing, returning or storing a
Stringmoves it: the handle is copied, the old variable becomes unusable, the heap data stays put. - Using a moved value is E0382, which points at the move and the later use.
- The compiler tracks moves through loops and branches and treats "maybe moved" as moved.
- A moved-from
mutvariable can be given a new value.
Going further: run rustc --explain E0382 now and read it again; it should all make sense. Then try moving a value inside a match arm and using it after the match.