all_lessons/Rust/04 · Move — assignment as transferlesson 5 / 23

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.

The thesis, here
Ownership is only as good as the operation that hands a value from one name to another. Make that operation copy the header and forget the source at compile time, and uniqueness survives every assignment, argument and return at the price of 24 bytes copied — the same 24 bytes a plain bitwise copy would have moved.
Linear position
Forced by: One owner, one drop: the language knows who frees each value. But assigning one owner to another copies its bytes, and a copied owner is a second owner of the same heap buffer — the double free is back. What must assignment mean for a value that owns something?
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?
The plan
Seven moves. (1) Enumerate what 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 = aWhat it doesCostWhat goes wrong
Copy the bits, both stay validcopies the 24-byte header24 bytestwo owners of one buffer: Lesson 03's double free
Copy the bits, count the ownerscopies the header, adds one to a counter that lives with the buffera counter, updated on every assignmenta cost paid by every value, shared or not (Lesson 03's third row)
Copy the buffer tooallocates a second buffer and copies the textan allocation and a copy proportional to the size, on every assignment, argument and returna hidden cost — the default in C++ (the copy constructor); see C++ · 06
Copy the bits, leave the source emptycopies the header and puts the source into a "moved-from" statethe source is still a live object: its destructor runs and it can still be touchedevery 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 sourcecopies the header; the source name is dead from here on24 bytes, and nothing at run time — no flag, no destructor for the dead namethe 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
}
Reading the error
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.

The move tracker
Each program is real Rust from this lesson. The machine runs it with the rules of Lesson 03 and, in parallel, the mini checker decides whether rustc would accept it (the same checker that tools/rust_verify/borrowck_fuzz.js compares with rustc on random programs). Dashed slots are names whose value moved or ended; orange moved here and red E0382 mark the two ends of a use-after-move. Where a program calls cond(), the button chooses what it returns.
names holding a value
—
names moved out
—
heap blocks live
—
would rustc accept it?
—
Edit the program
Show the core JS
function isCopyVal(v) {
  switch (v.t) {
    case 'int': case 'bool': case 'char': case 'unit': case 'str': return true;
    case 'ref': return !v.mut;
    case 'struct': return (world.structs[v.name].derive || []).indexOf('Copy') >= 0;
  }
  return false;
}
/* read a place used as an operand: Copy values are copied, everything else moves out */
function takeOperand(e) {
  var c = placeCell(e);
  if (c.st !== 'live') throw staticStop('use of ' + c.st + ' value `' + labelOf(e) + '`');
  if (isCopyVal(c.v)) return c.v;
  if (c.v.t === 'ref') return c.v;           // &mut is re-borrowed by the checker; here it is passed along
  // a place somebody else relies on cannot be left dead: the checker says E0507, and the machine stops at the same spot
  if (e.k === 'index') throw staticStop('cannot move out of index of `Vec`: the vector still owns the element');
  if (e.k === 'un' && e.op === '*' && isPlace(e.e)) {
    var bc = placeCell(e.e);
    if (bc.v.t === 'ref') throw staticStop('cannot move out of `' + labelOf(e) + '` which is behind a ' + (bc.v.mut ? 'mutable' : 'shared') + ' reference');
  }
  if (copyBits && needsDropVal(c.v)) { ev('copy', labelOf(e) + ' copied bit for bit — now two names hold the same header', { label: labelOf(e) }); return c.v; }
  var v = c.v;
  c.st = 'moved';
  ev('move', labelOf(e) + ' moved out', { label: labelOf(e) });
  return v;
}

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:

Look instead of take
&names[0] borrows the element and leaves it where it is. That is the next lesson.
Copy the price
names[0].clone() makes a second owner on purpose, with the cost at the call.
Leave something valid behind
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.

What a move does not cover
A move is a fact about names in one function: it says nothing about a value while it is being looked at by a second name, and it does not let two names share a value. Both need the borrowing of Lesson 05, checked against the law of Lesson 01. And everything above was checked by walking the paths of one function body; how such a walk works — it is the same one that will check borrows — is Lesson 07.

Common mistakes / failure modes

"A move copies the heap data"
It copies the header only: after moving a million-byte vector the buffer's address is unchanged (§2). Only clone copies the buffer.
".clone() is the neutral fix for any ownership error"
It removes the error by changing the program: a second buffer, and edits that never reach the original (add_one(v.clone()), §4). Ask what the function is for before you reach for it.
"Passing a value to a function just lets it look"
A call is an assignment: the argument moves in, and the parameter ends at the callee's brace (§2–§3). Looking without owning is a different kind of name.
"Copy is for small types"
It is for types that own nothing: a thousand-byte array is Copy; a struct holding one String is not, however small (§4).
"A moved-from name is gone for good"
It is dead until it is assigned again; and a name moved on only some paths is dead for the checker but carries a run-time drop flag (§3, §5).
"I can pull the first element out of a vector"
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

Try it
Load a function takes ownership. The checker rejects line 10. Repair it in three different ways by editing the source under Edit the program and pressing run, and for each repair say how many heap blocks are ever allocated: (a) pass a clone, 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?

Takeaway
Assigning an owner has five possible meanings; the one that costs nothing at run time and keeps the owner unique is to copy the header and kill the source name — a move. It copies 24 bytes, never the buffer. The compiler checks it as the mirror of definite assignment: a use of a dead name is 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

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).