Move — assignment as transfer
Lesson 03 gave every value one owner and ended it with its owner, and then found the ordinary statement let b = a; pushing back: copy an owner's bytes and there are two owners of one buffer. This lesson decides what assignment must mean instead. Of five possible meanings only one costs nothing at run time and keeps the owner unique: copy the header and make the source name dead, so the compiler knows at compile time that it may not be used again. That is a move. It has one escape hatch for types that own nothing (Copy), one explicit price for the rest (Clone), a run-time cost only when the compiler cannot decide (a drop flag), and one place it cannot go: out of somewhere that others still rely on.
New idea: assigning an owner transfers it: the bytes of the header are copied, and the source name is statically dead. The compiler tracks which names hold a value at every point — the analysis Lesson 02 used for "assigned before read", run in reverse — and rejects any use of a dead name (
E0382).Forces next: Assignment moves ownership, which makes ownership safe but awkward: a function that only wants to read a value must take it and hand it back. How can code use a value it does not own, and stay safe while the owner is still alive and free to change it?
let b = a; could mean and price each meaning. (2) Take the survivor apart: a header copy plus a dead name, and prove no buffer moves. (3) Read the compiler's half of the bargain, E0382, and see that arguments and returns are assignments too. (4) Give back the copies that were never dangerous (Copy) and make the dangerous ones explicit (Clone). (5) Meet the moves the compiler cannot decide, and the flag it keeps for them. (6) Find the sources that cannot die — the elements of a vector — and the idioms that leave something valid behind. (7) Count what the rule now makes awkward.1 · What could assignment mean for an owner?
Take the smallest program that strains Lesson 03's rule. a owns a heap buffer; let b = a; hands it to a second name; both names are printed afterwards:
fn main() {
let a = String::from("ada");
let b = a; // what does this mean for a name that owns a buffer?
println!("{a} {b}");
}
Rust rejects it. To see why that is the right answer and not merely one answer, list the meanings the statement could have had, and price each against Lesson 00's budget:
Meaning of b = a | What it does | Cost | What goes wrong |
|---|---|---|---|
| Copy the bits, both stay valid | copies the 24-byte header | 24 bytes | two owners of one buffer: Lesson 03's double free |
| Copy the bits, count the owners | copies the header, adds one to a counter that lives with the buffer | a counter, updated on every assignment | a cost paid by every value, shared or not (Lesson 03's third row) |
| Copy the buffer too | allocates a second buffer and copies the text | an allocation and a copy proportional to the size, on every assignment, argument and return | a hidden cost — the default in C++ (the copy constructor); see C++ · 06 |
| Copy the bits, leave the source empty | copies the header and puts the source into a "moved-from" state | the source is still a live object: its destructor runs and it can still be touched | every type must define a valid empty state, and every use of it must be checked at run time (C++11's move) |
| Copy the bits, kill the source | copies the header; the source name is dead from here on | 24 bytes, and nothing at run time — no flag, no destructor for the dead name | the compiler must know, at every use, whether the name is dead |
Row 1 is Lesson 03's unmanaged answer and row 2 its count, in miniature; row 3 is C++'s default. Row 4 is what C++ added when copying every string proved too expensive, and it is worth seeing because most readers already know it. The moved-from source stays a live object:
std::string a = "ada";
std::string b = std::move(a); // steals the buffer; a is now "valid but unspecified"
std::cout << a.size(); // compiles without a warning (Apple clang, -Wall); prints 0 here
The compiler accepts the last line because a is still an object whose destructor will run; that it is empty is what this library happens to do, and the rule the C++ track's Lesson 06 states is only "valid but unspecified". Every type must therefore define a moved-from state, and reading it wrongly is a bug the language cannot see. Row 5 is the only meaning that spends nothing at run time and keeps the owner unique, and its price is a fact the compiler must establish: for each name at each use, is it dead? Lesson 02 already built the tool. To prove a name is assigned before it is read, the compiler follows every path to the read; to prove it is not moved before it is used, it follows every path again, treating a move as the opposite of an assignment. Row 5 needs a name, and a proof that no buffer moves.
2 · What a move is
The word is chosen carefully: a move hands an owner from one name to another. Mechanically it is the header copy of row 1 — 24 bytes here for a String or a Vec, 8 for a Box — plus the compiler's decision that the source name no longer holds a value. It does not touch the buffer. That is testable, without printing an address: compare the buffer's pointer before and after, then after a clone, which is row 3 made explicit:
fn main() {
let a = vec![1u8; 1_000_000];
let before = a.as_ptr(); // where the million bytes live
let b = a; // a move: only the header is copied
println!("buffer moved? {}", b.as_ptr() != before);
let c = b.clone(); // a clone: a whole new buffer
println!("clone shares? {}", c.as_ptr() == b.as_ptr());
}
buffer moved? false
clone shares? false
A megabyte of data changed hands and not one byte of it was copied. The header may not even be copied in the optimized program — the standard library's documentation for Copy notes that both a copy and a move can result in bits being copied in memory, sometimes optimized away — and the only observable difference between a move and a copy is whether the source can still be used. Everything the next sections say is about that one difference.
Where do moves happen? Everywhere a value is assigned: a let, an assignment, a struct field initializer, a push into a vector, and — the case that will matter most — the arguments and results of function calls. A parameter is a name like any other, initialized by the argument, so calling shout(a) is let text = a; plus a jump. The callee's parameter then owns the value, and by Lesson 03's schedule the callee ends it at its closing brace, unless it hands the value on.
3 · The compiler's half: a dead name is a rejected name
Here is a function that consumes its argument and a caller that carries on using the name afterwards:
fn shout(text: String) -> usize {
println!("{}", text.to_uppercase());
text.len()
} // text ends here, and so does the buffer
fn main() {
let a = String::from("ada");
let n = shout(a); // the argument is an assignment: a moves into text
println!("{n}");
println!("{a}"); // a is dead
}
error[E0382]: borrow of moved value: `a`
--> src/main.rs:10:16
|
7 | let a = String::from("ada");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
8 | let n = shout(a);
| - value moved here
9 | println!("{n}");
10 | println!("{a}");
| ^ value borrowed here after move
|
note: consider changing this parameter type in function `shout` to borrow instead if owning the value isn't necessary
help: consider cloning the value if the performance cost is acceptable
The message has three anchors: where the name was declared and why it is not copyable, where the value left it, and the later use. Learn to read those three lines first; every ownership error in the next four lessons has the same anatomy. It also makes two suggestions, and they are a good specimen of advice that is correct as a local edit and silent about the design. The note says the function might not need to own its argument — that is the next lesson's whole subject. The help says to clone, which compiles, at the price of a second buffer, and (§4) sometimes at the price of a different program. The compiler cannot tell which of the two you meant, because the choice is a statement about what shout is for.Two facts make this rejection principled rather than fussy. First, a dead name is dead only until it is assigned again: after a = String::from("again"); (with let mut a) the name holds a value and may be used, exactly as after a first assignment — the Reference says a moved-from local is uninitialized until it is assigned. Second, the check is per name and per path, so it is local: no whole-program reasoning, only the function being compiled.
Step through it. The machine below runs the program with the real rules: a becomes a dashed slot the moment its value moves, no new block appears, and the callee's parameter ends — and frees the buffer — at shout's closing brace, well before the caller's last println! asks for a value that is gone.
4 · When nothing is owned: Copy, and the price of Clone
Killing the source is a price: it buys uniqueness. For a value that owns nothing there is no uniqueness to protect. Copy an i32 and you have made no second owner of anything, because there is nothing to end; keeping the source alive costs nothing and forbidding its use would be pointless. So a type can declare itself Copy: assigning it copies the bits and leaves the source usable. (#[derive(Clone, Copy)] asks the compiler to write the trait impls; traits are Lesson 11's.) The declaration is checked against the reason for it:
#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 } // owns nothing: a copy is harmless
struct Label { text: String } // owns a buffer: a copy would be a second owner
fn main() {
let p = Point { x: 1, y: 2 };
let q = p; // copy: p is still usable
let l = Label { text: String::from("origin") };
let m = l; // move: l is dead
println!("{} {} {}", p.x, q.y, m.text);
}
1 2 origin
#[derive(Clone, Copy)]
struct Label { text: String } // a Copy type must hold only Copy fields
struct Guard { name: &'static str }
impl Drop for Guard {
fn drop(&mut self) { println!("drop {}", self.name); }
}
impl Clone for Guard { fn clone(&self) -> Guard { Guard { name: self.name } } }
impl Copy for Guard {} // a type with a destructor cannot be Copy
Both rejections are Lesson 03's rule restated. A field that owns something, like a String, is not Copy, so copying the struct would copy an owner (E0204); a destructor means the value manages a resource beyond its own bytes, and a copy would run the destructor twice (E0184). Copy is therefore not a statement about size — an array of a thousand bytes is Copy and passing it copies a thousand bytes — but about ownership: nothing owned, nothing to duplicate, nothing to end. Shared references are Copy, because a view owns nothing (Lesson 05); an exclusive one is not, because it must stay unique.
For the types that own something, the way to say "I really want a second one" is to say it: Clone. a.clone() runs the type's own code, allocates a second buffer and copies the text (row 3, chosen instead of inherited), and the price is visible in the source, at the call. That visibility is the whole design: Rust never makes a deep copy implicitly, so any copy the language does for you is assumed cheap. The trap is to treat .clone() as a neutral cure for E0382. It cures the error by changing the program, and the new program may be slower — or wrong:
fn add_one(mut list: Vec<i32>) { list.push(1); }
fn main() {
let v = vec![10, 20];
add_one(v.clone()); // compiles — and edits a copy that is thrown away
println!("{:?}", v); // still [10, 20]
}
[10, 20]
The error is gone and the caller never sees the push. Which of "give the function my vector", "give it a copy" or "let it look at mine" is right depends on what the function is for; the machine makes the price concrete, since a clone adds a second heap block where a move adds none.
What to try. Load use after move: the checker flags line 4 and the run stops there, with the value's block already owned by b and a dashed. Load a function takes ownership and step to the call: the callee's frame appears with text holding the header, a is dashed in main, and the block is freed when shout returns. Now use the Edit the program box for the checkpoint below. Load clone and step to its print (step 3): two blocks are live, against one at the print of assignment moves (step 4). Load Copy and not Copy: the same-looking let q = p; and let m = l; leave p alive and l dashed. Every program so far had an answer the compiler could read off the text. What if the answer depends on the run?
5 · When the move depends on the run
The check so far assumed the compiler can tell, at each use, whether a name is dead. Control flow can make that depend on the run. Move a value inside an if and the name is dead on one path and alive on the other:
struct Guard { name: &'static str }
impl Guard {
fn new(name: &'static str) -> Guard { println!("make {name}"); Guard { name } }
}
impl Drop for Guard {
fn drop(&mut self) { println!("drop {}", self.name); }
}
fn main() {
let flag = std::env::args().count() > 1; // unknown until the program runs
let g = Guard::new("g");
if flag { drop(g); } // g is moved on this path only
println!("end of main");
} // ...so who drops g here?
make g
end of main
drop g
Run with no arguments it prints drop g at the brace; run with one and the drop comes from inside the if, and the brace prints nothing. Both must be right, and the compiler cannot say which at compile time. So it does the only thing left: it keeps a hidden boolean, a drop flag, for g on the stack, clears it when g moves, and tests it at the brace. This is the one place where a move costs anything at run time, and only for a name whose fate is conditional — straight-line code, and branches that treat the name alike, are resolved statically and carry no flag. (The Nomicon calls these drop flags and says exactly this.) Toggle cond() in the machine on a conditional move and watch the brace: drop g from the scope on one setting, g was moved: nothing to drop on the other.
What may the program do with such a name afterwards? Not use it: dead on any path is dead for the checker, which is sound and therefore conservative. A loop makes the same point without a condition: a move inside the body is a move on every iteration after the first.
fn main() {
let s = String::from("ada");
for _ in 0..2 {
drop(s); // the second time round, s is already dead
}
}
The message adds "in previous iteration of loop" to the same three anchors. Moves are only ever legal from a name that is alive; when a value must leave a place that cannot be left dead, the compiler has nothing left to offer.
6 · Sources that cannot die
A move kills its source, so it is only legal from a place the program is free to leave empty: a local that is not borrowed, a temporary, a field of such a place, the target of a Box. Now try to move a String out of the middle of a vector:
fn main() {
let names = vec![String::from("ada"), String::from("bob")];
let first = names[0]; // would leave a hole in the vector's buffer
}
Suppose it were allowed. names[0] would be dead, but nothing in the type system marks the element dead: the vector still believes it owns two strings, and at the brace it would drop both — the first one twice. A place that someone else relies on (an element, a value behind a reference) cannot be left dead, so the compiler refuses to move out of it (E0507; an array element is E0508, a field of a type with a destructor E0509, for the same reason). The three ways out each say what happens to the place:
&names[0] borrows the element and leaves it where it is. That is the next lesson.names[0].clone() makes a second owner on purpose, with the cost at the call.mem::take, mem::replace and mem::swap put a value in the place as they take the old one out; Vec::remove closes the gap; Option::take (Lesson 09) is the same trick for an optional value.use std::mem;
fn main() {
let mut names = vec![String::from("ada"), String::from("bob")];
let a = mem::take(&mut names[0]); // leaves "" behind
let b = mem::replace(&mut names[1], String::from("?")); // leaves "?" behind
let mut x = a;
let mut y = b;
mem::swap(&mut x, &mut y); // exchanges two places; both stay valid
println!("{x} {y} {names:?}");
}
bob ada ["", "?"]
(The &mut here just names the place for the function; what it means is Lesson 05's.) swap needs no spare value at all, because it puts each value where the other was. take works because there is always a value it can leave behind: the empty string. Ask it of a type that has no obvious empty state and the compiler names the missing piece:
use std::mem;
struct Token { id: u32 }
fn main() {
let mut slot = Token { id: 7 };
let old = mem::take(&mut slot); // what value should be left in slot?
}
The trait is Default — "a valid value to leave behind" — and the compiler's suggestion, #[derive(Default)], is a real design choice: it declares that a zero Token is a legitimate one. mem::replace takes the replacement as an argument instead and asks nothing of the type. A field is a place that can be left dead when its parent is not borrowed and has no destructor, so a struct can be dismantled piece by piece: after let n = p.name; the rest of p is still usable, and it is dropped field by field — only the part that is left. That is a partial move (using p whole is E0382, "partially moved"; a type with a destructor forbids it, E0509).
struct Person { name: String, age: u32 }
fn main() {
let p = Person { name: String::from("Ann"), age: 30 };
let n = p.name; // moves one field out
println!("{n} {}", p.age); // the other field is untouched
}
Ann 30
One more consequence of "arguments are assignments": operators are functions, so they move too. a + &b on strings takes a by value, and a is dead afterwards, exactly like the argument of shout:
fn main() {
let a = String::from("ada");
let b = String::from("bob");
let c = a + &b; // a is consumed; b is only looked at
println!("{c} {b}");
println!("{a}"); // a is dead
}
7 · What the rule leaves awkward
The design is complete: assignment transfers, the compiler tracks the dead, Copy covers what owns nothing, Clone names the price, a flag covers the undecidable. And it has one obvious cost, which the error message of §3 already hinted at. Every function that touches a value takes it, so a function that only wants to read a value — measure it, print it, compare it — must be handed ownership and give it back, or the caller loses the value:
fn measure(text: String) -> (String, usize) {
let n = text.len();
(text, n) // hand it back, or the caller loses it
}
fn main() {
let a = String::from("ada");
let (a, n) = measure(a); // shadowing rebinds the name; the pair is unpacked (Lesson 09)
println!("{a} {n}");
}
ada 3
This is safe — exactly one owner at every moment, exactly one drop — and it is clumsy: a length function that must return its argument. The cost is not run time (a move is a header copy) but thinking, and it compounds: a function with three inputs must return all three. What the function needs is not ownership. It needs to look at the value while the caller stays the owner, and to be able to do so safely when the owner is still alive and free to change the value in the meantime. That is a new kind of name — one that can reach a place without being responsible for ending it — and it is exactly what Lesson 01's law was written to govern.
Common mistakes / failure modes
clone copies the buffer..clone() is the neutral fix for any ownership error"add_one(v.clone()), §4). Ask what the function is for before you reach for it.Copy is for small types"Copy; a struct holding one String is not, however small (§4).names[0] as a value would leave a hole the vector does not know about. Borrow it, clone it, or take it and leave something valid behind (§6).Checkpoint exercise
shout(a.clone()); (b) move the last println! above the call; (c) change shout to return its argument — -> String, with text as the last line — rebind in main with let a = shout(a);, and print a.len() where n was. Then decide which repairs change what the program means. Answer: all three make the checker say yes. (a) allocates two blocks — one for a, one for the clone, which is freed inside shout; (b) and (c) allocate one, moved through the whole run. Only (b) changes the output, from ada 3 ada to ada ada 3, because the print moved. (a) stays harmless only while shout merely reads its argument (§4's add_one does not), and (c) is the take-and-give-back shape of §7: one owner throughout, and the ugliest signature.Where this points next
Assignment now transfers an owner, and the compiler keeps the ledger: at every use, each name either holds a value or is dead. That makes ownership airtight, and it makes every call a hand-over. The idioms that soften it — clone, take, returning the argument — all work by giving the value away or duplicating it; none lets a function read a value the caller keeps. The missing piece is a second kind of name that reaches a place without being responsible for ending it. Such a name is safe only if the owner cannot end or change the value out from under it, which is Lesson 01's law restated for a program with an owner. So how can code use a value it does not own, and stay safe while the owner is still alive and free to change it?
E0382, and arguments, returns, pushes and even a + &b are moves. Copy is for types that own nothing (no destructor, only Copy fields), Clone is the explicit, priced duplicate — not a neutral cure for errors. A move whose path depends on the run gets a hidden boolean drop flag; a place others rely on (a vector element) cannot be moved out of (E0507), so you borrow, clone, or take/replace leaving a valid value (Default). Ownership is now safe and awkward: a function that only reads must take and return.Interview prompts
- What does
let b = a;do whenais aString, and why doesn't Rust copy the heap data? (§1–§2 — it copies the 24-byte header and marksadead at compile time; a deep copy would be an implicit allocation and an O(n) copy on every assignment, argument and return; the pointer test shows the buffer never moves.) - How does a Rust move differ from a C++
std::move? (§1 — C++ leaves the source alive in a valid-but-unspecified state whose destructor still runs; Rust makes the source name statically dead, so no destructor and no use, and no run-time trace except a drop flag when the move is conditional.) - Why is using a moved value a compile error rather than a run-time check? (§3 — the compiler already follows every path to check definite assignment; treating a move as un-assigning the name gives the same per-function analysis, so nothing has to happen at run time.)
- What are
CopyandClone, and why can't a type with aDropimpl beCopy? (§4 — Copy is an implicit bitwise copy for types that own nothing; Clone is an explicit, possibly expensive duplicate; a Copy type with a destructor would run it twice, E0184, and a Copy type may hold only Copy fields, E0204.) - Is
.clone()a safe fix forE0382? (§4 — it removes the error by changing the program: extra allocation and copy cost, and edits that go to the copy; theadd_one(v.clone())example compiles and silently does nothing.) - What is a drop flag and when does the compiler need one? (§5 — a run-time boolean for a name that is moved on some paths and not others, tested at scope end; straight-line and uniformly treated names need none.)
- Why can't you move a
Stringout ofv[0], and what can you do instead? (§6 — the vector would still believe it owns the element and drop it twice, E0507; borrow it, clone it, or usemem::take/mem::replace/Vec::removewhich leave a valid value or close the gap.) - Passing a value to a function moves it. What awkwardness follows, and what does it point at? (§7 — a function that only reads must take and return the value, safe but unwieldy; what is missing is a name that can reach a place without owning it.)
Companion reads: C++ · 06 Copying and moving (rows 3 and 4 of §1 in C++), C++ · 07 Ownership and smart pointers, Scala · 03 Immutability (sharing without ownership, by never writing), and Go · 04 Structs, methods and receivers (value versus pointer receivers, the same question with a collector).