Part 1 · The runtime machine
The four verbs: move, Copy, Clone, Drop
Everything that can happen to a value when it changes hands. Three of these are free; the one people fear is the one they reach for most.
What this lesson is trying to break (4)
- `Copy` means "this type gets copied instead of moved".
- `.clone()` is always expensive.
- You can call `.drop()` on a value to destroy it early.
- Cloning to satisfy the borrow checker is always a hack.
Before any explanation, try this.
Here is a function that does not compile:
fn total_length(items: Vec<String>) -> usize {
let mut n = 0;
for s in items {
n += s.len();
}
println!("{} items measured", items.len()); // error here
n
}Without looking anything up: why does the last line fail, and write down two different fixes. Then say which fix you would choose and what it costs.
Do not skip this. Getting it wrong now is worth more than reading the answer cleanly — attempting a problem before instruction reliably beats being taught first, as long as you actually attempt it.
The for s in items loop consumes items. for calls IntoIterator::into_iter,
and for Vec<String> that takes the vector by value — so the loop owns it,
and after the loop the binding is dead. Two obvious fixes are for s in &items
(borrow instead of consume) and calling items.len() before the loop. A third
is items.clone(), which works and costs a full deep copy of every string.
Which one is right depends on the four verbs.
The four verbs
Every time a value changes hands in Rust, exactly one of these happens.
Varies: what happens to the original — the destination always ends up with a usable value
| Verb | Written as | Source afterwards | Cost | Chosen by |
|---|---|---|---|---|
| Move | let b = a; | dead — compiler refuses use | shallow byte copy, often optimised away | the compiler, by default |
| Copy | let b = a; | still alive | identical to a move | the type, via impl Copy |
| Clone | let b = a.clone(); | still alive | whatever clone does — free to arbitrary | you, explicitly |
| Drop | end of scope | destroyed | whatever drop does | the compiler, at scope end |
Notice that Move and Copy are the same operation. They differ only in whether the compiler kills the source afterwards.
That last line is the insight, and it is the opposite of how Copy is usually
described.
Copy does not mean “copied instead of moved”
Rust has two kinds of assignment. Big types like String get moved, and
small types like i32 get copied — a different, cheaper operation chosen
because they are small.
There is only one operation. let b = a; always does the same thing: copy
the value’s bytes into the new place. Size has nothing to do with it —
[u8; 4096] is Copy and Box<u8> is not.
Copy is a claim about correctness, not about cost. It says: duplicating
these bytes yields a second, equally valid value. When that claim holds, the
compiler has no reason to invalidate the source, so it does not. Move is
what happens when the claim does not hold.
Ask why String cannot be Copy and the answer falls straight out of
1.1. Duplicating a String’s three words
gives you two headers pointing at one heap buffer. Both would free it. The
bytes duplicated fine; the meaning did not.
So the rule is:
A type can be
Copyonly if duplicating its bytes is semantically harmless — which means it owns no resource that needs releasing.
And therefore a type can never be both Copy and Drop. The compiler enforces
this directly:
#[derive(Clone, Copy)]
struct Guard;
impl Drop for Guard { // error[E0184]: the trait `Copy`
fn drop(&mut self) {} // cannot be implemented for this type;
} // it has a destructor
#[derive(Clone, Copy)]
struct Point { x: f64, y: f64 }
struct Label { text: String }
fn main() {
let p = Point { x: 1.0, y: 2.0 };
let q = p;
println!("{} {}", p.x, q.x); // line A
let l = Label { text: "hi".into() };
let m = l;
println!("{} {}", l.text, m.text); // line B
}Line A is fine. Line B fails with E0382.
Point is Copy, so let q = p leaves p alive. Both are usable.
Label contains a String, so it cannot be Copy — and note that you could
not have derived Copy for it even if you wanted to, because String is not
Copy. Copy is only derivable when every field is Copy. So let m = l
moves, and l is dead.
Two things worth internalising:
Copyis contagious upward but not downward. A struct can beCopyonly if all its fields are.- The move in line B moved the whole struct, not just the field. There is no
partial state where
l.textis dead butlis otherwise alive — though confusingly, Rust does support partial moves out of a struct (let t = l.text;moves just the field and leaves the rest usable). The compiler tracks this per-field.
Clone is not “expensive copy”
The second bad heuristic. Clone means duplicate this correctly, and how
expensive that is depends entirely on the type.
Varies: only the type being cloned — the call is `.clone()` in every row
| Type | What .clone() does | Cost |
|---|---|---|
i32 | copies 4 bytes | free (and redundant — it is Copy) |
Rc<T> / Arc<T> | copies a pointer, increments a counter | very cheap — no T is duplicated |
String | allocates, copies the bytes | proportional to length |
Vec<String> | allocates, then clones every element | proportional to total size |
&T | copies the reference | free — references are Copy |
`Arc::clone` and `Vec::clone` are the same method name and differ by orders of magnitude. This is why reviewers ask what is being cloned rather than whether cloning is happening.
Because of this, Arc::clone(&x) is often written in preference to x.clone()
even though they are identical. The explicit form tells the reader this is a
refcount bump, not a deep copy. That is the social machine at work — a
convention carrying information the type system already had.
And a practical note for someone coming from garbage-collected languages, since this comes up constantly in code review:
Cloning to get past the borrow checker is not automatically wrong. A
.clone()of a short string at a boundary, once per request, is invisible. The same call inside a hot loop is not. “Don’t clone” is not a rule — “know what you cloned and how often” is.
Being able to say that calmly is a tech-lead skill. Rust newcomers tend to oscillate between cloning everything and refusing to clone anything, and both are expensive in different ways.
Drop, and the empty function that destroys things
Drop is Rust’s destructor. It runs automatically when a place goes out of
scope, and you cannot call it yourself:
let s = String::from("hi");
s.drop(); // error[E0040]: explicit use of destructor method
You call drop(s) instead — and this is where the mechanism becomes clear,
because here is the entire implementation from the standard library:
pub fn drop<T>(_x: T) {}
An empty body. It works because the parameter is taken by value: calling
drop(s) moves s into _x, and _x’s place ends immediately at the closing
brace. The destruction comes from the move plus the scope end — nothing in the
function does anything at all.
If that made sense on first reading, Part 1 has landed. If it did not, go back to 1.1 rather than pressing on; everything in Part 2 assumes it.
For Go programmers there is a clean comparison: Drop is defer, except it
is attached to the type instead of written at the call site, so it cannot be
forgotten. That is the RAII pattern, and it is what makes MutexGuard,
File and Vec all release their resources without any explicit close.
Return to total_length at the top of this lesson. You now have four verbs.
Rewrite the signature and the loop so nothing is cloned and nothing is consumed, then explain in one sentence what changed about who owns the vector.
What you should now be able to say
- Move and
Copyare the same machine operation; they differ in whether the source stays usable. Copyis a claim that a bitwise duplicate is correct, not that it is cheap. Hence: no type is bothCopyandDrop.Cloneis explicit and its cost is the type’s business —Arc::cloneis a counter increment,Vec<String>::cloneis a deep copy.drop(x)destroysxby moving it into a function whose body is empty.Dropisdeferbound to the type instead of the call site.
Next: 1.4 — Where values die, which is about the exact moment drop happens, and the deadlock that catches everyone once.