Ownership — one owner, one drop
Lesson 02 ended at the edge of the frame: a value whose size is decided while the program runs lives on the heap, and nothing there ends a place by itself. This lesson asks who should. There are four candidates — the programmer, a collector, a count kept on every value, or a name that already ends exactly once — and only the last costs nothing at run time. It becomes a two-part rule: every value has exactly one owner, and a value is dropped exactly when its owner ends. The rest follows from it: what a drop does, in what order, what it does not promise, and the one operation the rule makes hard.
New idea: give every value exactly one owner, and let the owner's end be the value's end. "At most once" holds because there is only one owner; "at least once" holds because the owner's scope ends (§6 lists the exceptions). The compiler inserts the ending — the drop — so no line of the program says "free".
Forces next: 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?
String, Vec and Box are fixed-size headers with a buffer behind them. (4) Make an ending visible with Drop. (5) Derive the order in which one brace ends several values, and run it in a machine. (6) Say what drop does not promise. (7) Price the rule, and find the operation it makes hard.1 · Who ends a heap value?
Start with the smallest program that reaches the gap. String::from asks for a place at run time, sized for the text it is given:
fn main() {
let name = String::from("ada"); // three bytes of text, in a heap place made now
println!("{name}");
} // ...who returns those three bytes?
name's own slot ends at the brace — Lesson 02's frame sees to that — but the text is not in the frame. Someone must return it, and exactly once: never is a leak, twice corrupts the allocator (a double free), and once while a second name can still reach it is a use-after-free. These are the C++ track's three manual-memory failures (Lesson 04); the last two are Lesson 01's hard row. "Someone" can be four things:
| Who ends it | How it knows when | What it costs against Lesson 00's budget | What it leaves open |
|---|---|---|---|
| The programmer | reads the code and writes free on every path out | nothing at run time; unbounded thinking, because every new return is a new place to forget | all three hazards, invisibly |
| A collector | a run-time component finds which values are still reachable | a run-time system, which the budget rules out as the default; values end when it decides, not where the program's structure says | memory only — a file or a lock is released by code you still write. (The Go track argues its case.) |
| A count on every value | the last name to go ends it | a counter stored with each value and updated on every copy and every end — a cost in every use, not only at the end | two values that count each other never end |
| A name that already ends exactly once | its own end: a slot at its brace | nothing beyond the free itself | the compiler must know which name it is, for every heap value |
The first row is what C and C++ hand you. Counterexample first: two names for one buffer, and nothing in the program says which one frees.
Buffer* a = new Buffer(64);
Buffer* b = a; // a second name; nothing says which one frees
delete a;
delete b; // double free: undefined behavior
The last row is the C++ track's RAII (Lesson 05): tie a heap value's end to a stack value's end, and the language's own scope rule does the freeing. In C++ that is a discipline — new and raw pointers still exist, and a wrapper can be copied by accident. Rust makes it the only way to own a heap value in safe code, and the compiler checks it. The rest of Part II is the work that checking takes.
Rc and Arc (Lesson 15). What Rust refuses is making it the price of every value, including the ones nobody shares.2 · The rule: one owner, one drop
Turn the last row into something a compiler can check. It has two halves, and each is forced by a failure the other rows showed:
Why exactly one? Two owners can each end the value, and then either one ends it while the other is still reachable, or both do: the C++ snippet above. Zero owners is the leak: a value nobody is responsible for is never ended. "One" is the only count for which the ending is well defined, and it is checkable, because a place either holds a value or does not.
Look at what the rule leaves out: it does not say the owner is a stack slot. An owner may sit inside another value — a field, a vector slot, a box — and then it ends when that value is dropped. Drop is recursive; endings nest as the values do, so a single closing brace can release a whole tree. The vocabulary is fixed here for the rest of the series: an owner is the one name responsible for ending a value, and to drop is to end it (we say "free" only for the raw heap buffer).
| C++ · trusts you | Go · constrains you | Rust · verifies | |
|---|---|---|---|
| Who ends a heap value | you, or a wrapper you chose to use | the collector | its owner, fixed at compile time |
| When | at the delete, or the wrapper's destructor | some time after the last reference goes | exactly where the owner's scope ends |
| What you can get wrong | forget it, repeat it, use it after | timing; non-memory resources | the design: who should own this? |
The rule is a promise the compiler can keep only if it always knows which name is the owner. Before asking how it knows, look at what an owner actually holds.
3 · What an owner holds: a header, and a buffer behind it
An owner has to fit in a Lesson 02 slot, so it is fixed-size, whatever it owns. The three owners you will meet first:
use std::mem::size_of;
fn main() {
println!("String {}", size_of::<String>());
println!("Vec<u8> {}", size_of::<Vec<u8>>());
println!("Box<i32> {}", size_of::<Box<i32>>());
println!("&str {}", size_of::<&str>()); // a view, not an owner
}
On this machine (2026-09, aarch64) a String and a Vec<u8> are 24 bytes and a Box is 8: a header in the slot, and the variable-sized part elsewhere. The documentation describes a String as a pointer, a length and a capacity, and promises that Vec<T> is a (pointer, capacity, length) triplet, field order unspecified. A Box<T> is one pointer to one value on the heap. The &str line is there for contrast: 16 bytes, but an &str owns nothing — it is Lesson 02's view — so dropping it ends nothing.
So "the owner ends" means "the header's slot ends", and the header's drop is what releases the buffer: String and Vec free theirs; a Box drops its target and then frees. The capacity is why most pushes are cheap and some are not. While the length is below the capacity a push writes into room that is already there; when they meet, the vector gets a bigger buffer, moves its elements across and releases the old one:
fn main() {
let mut v: Vec<u32> = Vec::new();
let mut cap = v.capacity();
println!("cap {cap}"); // an empty Vec owns no buffer yet
for i in 0..9 {
v.push(i);
if v.capacity() != cap { // this push needed a bigger buffer
cap = v.capacity();
println!("len {} cap {cap}", v.len());
}
}
}
cap 0
len 1 cap 4
len 5 cap 8
len 9 cap 16
The 4, 8, 16 are what this build does; the documentation promises only that Vec::new() does not allocate and that a push at len == capacity reallocates. The fifth push released the old buffer, and nothing you wrote did: the vector owns it, so replacing it is the vector's business — ownership paying for itself for the first time. (A name still holding the old buffer's address is Lesson 01's iterator-invalidation row; Lesson 05 stops it.)
Ownership nests, so a type can own a value of its own type only if the size stays finite — and the compiler measures every local's size at compile time. Ask for the infinite one:
struct Node {
value: i32,
next: Node, // a Node that contains a Node that contains a Node...
}
error[E0072]: recursive type `Node` has infinite size
--> src/main.rs:1:1
|
1 | struct Node {
| ^^^^^^^^^^^
2 | value: i32,
3 | next: Node,
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
3 | next: Box<Node>,
| ++++ +
It names the type, points at the field that closes the loop, and offers three ways out. It cannot know which one you mean, because they are three different answers to this lesson's question. Box says the node owns the next one (one owner, dropped together); Rc says several nodes may own it (Lesson 15); & says this node owns nothing and someone else does (Lessons 05–08). Who owns the next node is a design decision, and reaching for & or Rc by reflex buys a lifetime or a counter you may not need.The Box version is a finite type — a value and one pointer — and it shows what indirection costs in space (16 bytes: 12 of fields, rounded up to a multiple of 8):
use std::mem::size_of;
struct Node {
value: i32,
next: Box<Node>, // an owner: 8 bytes here, the next node on the heap
}
fn main() { println!("Node {}", size_of::<Node>()); }
(A list also needs a way to end, and "no next node" cannot be a Box; representing absence is Lesson 09's job.) Now the header and the buffer are clear, and the drop that ties them together needs to be something we can watch.
4 · Drop is code: RAII, made compulsory
To see an ending happen we need a type whose ending prints. Drop is the hook: write impl Drop for Guard with its one method, drop, and the compiler calls it when a Guard is dropped — the C++ destructor, with the calling made compulsory and automatic. (An impl block attaches functions to a type; &mut self names the value a method runs on, Lesson 05's subject.) The order inside one drop is fixed: the type's own drop runs first, then its fields are dropped, then the memory it owns is released.
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 a = Guard::new("a");
let b = Guard::new("b");
println!("end of main");
} // both end here; nobody wrote a call
make a
make b
end of main
drop b
drop a
Nothing in main mentions drop; the two lines after end of main are the compiler's insertion at the brace. The same hook is how a file closes, a lock releases and a connection hangs up: any resource with an end can be made an owner.
Can you do the compiler's job yourself? The compiler forbids it, and the reason follows from the rule:
struct Guard { name: &'static str }
impl Drop for Guard {
fn drop(&mut self) { println!("drop {}", self.name); }
}
fn main() {
let g = Guard { name: "g" };
g.drop(); // the compiler will drop g again at the brace
}
If g.drop() were allowed, the drop at the brace would end g a second time: the rule's "one drop" would be broken by the very code the rule exists to protect. The function the compiler suggests, drop(g), is not a special ending; it is an ordinary function that takes its argument and lets it end:
struct Guard { name: &'static str }
impl Drop for Guard {
fn drop(&mut self) { println!("drop {}", self.name); }
}
fn end<T>(_x: T) {} // std's drop is exactly this
fn main() {
let g = Guard { name: "g" };
let h = Guard { name: "h" };
end(g); // g is handed over; the callee's parameter ends at its brace
println!("after end(g)");
drop(&h); // hands over only a reference: nothing to end
println!("after drop(&h)");
}
drop g
after end(g)
after drop(&h)
drop h
end(g) prints drop g before the next line: the value was handed to the function, whose parameter ended at the function's brace. (How a value gets handed over, and what happens to the name g afterwards, is the whole of Lesson 04; here only the effect matters.) drop(&h) ends nothing, because you can only end what you own, and a reference owns nothing. rustc says so:
warning: calls to `std::mem::drop` with a reference instead of an owned value does nothing
help: use `let _ = ...` to ignore the expression or result
The compiler's suggestion silences the warning and keeps the misunderstanding — a good specimen of the local edit that is not a design. With endings visible, the next question is what happens when many values end at one brace.
5 · In what order? The schedule, and a machine to run it
Several values ending at one brace need an order, and an arbitrary order would make programs unpredictable — a resource closed while another still needs it. The rule for locals follows from Lesson 01: a later value may hold a name for an earlier one (a view of its buffer, a guard on its lock), so it must end first. That is the reverse of declaration order, and the guard example already showed it (b before a). The other cases are decisions the language documents:
| What ends | Order | Why it is that way |
|---|---|---|
| Locals of one scope | reverse of declaration | a later local may depend on an earlier one |
| Fields of a struct | declaration order — not the order written in a struct literal | one fixed schedule per type, whoever constructs it |
| Elements of a tuple or array | first to last (guaranteed); a Vec does the same today, but its documentation declines to promise it | the natural reading order |
| A temporary | at the end of the statement that made it, in reverse order of creation | a value nobody named has nobody to end it later |
let _ = make(); | immediately, at the semicolon | _ binds no name, so nothing owns the new value afterwards |
let _kept = make(); | at the end of the scope, like any name | an ordinary binding whose name merely starts with an underscore |
Assigning x = new | the old value ends, after new is computed | the slot is about to hold something else |
Read that table against a real run. One program exercises the underscore, the temporary, the assignment and the field order at once; its last statement builds a Pair whose literal names the fields in the opposite order from the declaration:
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); }
}
struct Pair { first: Guard, second: Guard }
fn main() {
let _ = Guard::new("ignored"); // `_` binds no name: dropped at the `;`
let _kept = Guard::new("kept"); // a name: lives to the closing brace
Guard::new("temp"); // a temporary: dropped at the `;`
let mut slot = Guard::new("old");
slot = Guard::new("new"); // the old value ends when it is overwritten
let _pair = Pair { second: Guard::new("second"), first: Guard::new("first") };
println!("end of main");
}
make ignored
drop ignored
make kept
make temp
drop temp
make old
make new
drop old
make second
make first
end of main
drop first
drop second
drop new
drop kept
Six of the rows are there. ignored and temp end at their own semicolons; drop old comes after make new, because the right-hand side is computed before the slot is overwritten; make second precedes make first (the literal's order) while drop first precedes drop second (the declaration's); and at the brace the locals end in reverse — _pair, slot (now holding new), _kept. The field row is the one that surprises a C++ programmer, so the same experiment in C++:
struct Pair { G first; G second;
Pair() : second("second"), first("first") {} };
// prints: make first / make second / built / drop second / drop first
Rust evaluates in the order the source states and drops in the order the type states; C++ makes members in declaration order and destroys them in the reverse. The machine below checks a hand-traced schedule like this one. It is the mini-Rust engine (minirust.js) that also powers Lessons 04–08, and its drop order is differential-tested against real programs compiled by rustc, so the trace is the compiler's schedule, not a guess.
What to try. Load the schedule and, before touching the slider, write down the fifteen lines it prints; then compare with the snippet's output above. The slider shows why: Guard::new("ignored") ends at step 3, before the next line runs; make new (step 18) comes before drop old (step 21) because the right-hand side is computed first, then the old value ends, then the slot takes the new one (step 22); and at the brace (steps 29–41) the _pair slot ends first, its two fields in declaration order, then slot, then _kept. Load String and Vec and watch the fifth push (steps 8–10): a new block appears, the old one turns dashed, the arrow moves and cap goes from 4 to 8. Load Box: at the brace the box's target ends first (step 12), then the box's own block is freed (step 15). Every row assumed that the owner's end runs. Does it always?
6 · What drop does not promise
Two things skip a drop, and both compile without unsafe:
use std::mem;
struct Guard { name: &'static str }
impl Drop for Guard {
fn drop(&mut self) { println!("drop {}", self.name); }
}
fn main() {
let a = Guard { name: "a" };
let _b = Guard { name: "b" };
mem::forget(a); // takes ownership and never drops
println!("end");
}
end
drop b
struct Guard { name: &'static str }
impl Drop for Guard {
fn drop(&mut self) { println!("drop {}", self.name); }
}
fn main() {
let _g = Guard { name: "g" };
println!("exiting");
std::process::exit(0); // ends the process; no destructor runs
}
exiting
Why is mem::forget safe? Because Lesson 01's promise is "no undefined behavior", and a leak cannot cause any: the memory just stays allocated. The Reference lists leaks of memory and other resources, and exiting without calling destructors, among the behaviors that are not considered unsafe; a cycle of Rcs (Lesson 15) leaks the same way. The consequence for a library author is sharp: a destructor is not a place to keep a soundness argument, because it may never run. Lessons 17 and 20 pay that bill.
So "the language knows who frees each value" is exact, and modest: nothing frees a value twice, nothing frees it while its owner is alive, and a value whose owner ends is dropped. What is not promised is that every owner ends normally. Panics unwind and run the drops (Lesson 10); an aborting panic, process::exit and a forgotten value do not. That is the guarantee; what does it cost?
7 · What it costs, and the one thing it makes hard
The budget said no hidden cost, so measure the ending. Here is everything rustc 1.98.1 generates at -C opt-level=3 on aarch64 for dropping a Vec<u64> that is handed in:
pub fn end_of_scope(v: Vec<u64>) {
drop(v);
}
end_of_scope:
ldr x8, [x0] ; the capacity, from the header
cbz x8, done ; capacity 0: nothing was ever allocated
ldr x0, [x0, #8] ; the pointer
lsl x1, x8, #3 ; the size in bytes: capacity * 8
mov w2, #8 ; the alignment
b __rust_dealloc ; hand the block back
done:
ret
One load, one compare-and-branch, and the allocator's free: what a hand-written release of a growable array would do, plus a test for the empty vector that owns no buffer. The four currencies: run time, the free itself; memory, no counter or header beyond the ones the value needed anyway; compile time, the compiler must know each value's owner at every point (the bill Lessons 04–08 itemize); thinking, you can no longer share without saying how.
And there is one operation the rule turns from trivial into a question. Lesson 02 made a place hold bytes, and let y = x read one place and wrote another: copy the bytes. Do that with an owner. A String is a 24-byte header; copy the 24 bytes and there are two headers with one pointer, hence two owners of one buffer — and at the closing brace both end. Turn on assignment copies the header in the machine and load the last program (hypothetical: Rust does not do this) to watch the run stop: block #1 is freed by b, and then a frees it again. The rule said "one owner"; the most ordinary statement in the language, an assignment, just made two.
String is not its owner, and neither is a struct that holds a view of a buffer; those names are Lessons 05–08.Common mistakes / failure modes
drop(&h) did nothing (§4). A value ends when its owner ends; counting names is for the library types Rc and Arc, which you choose (Lesson 15).drop b, then drop a. Fields are the opposite direction: declaration order, whatever order the struct literal was written in (§5).let _ = make(); keeps the value, like let _guard = make();"_ binds nothing, so the value ends at the semicolon; _guard is a name, so it lives to the brace. A lock guard bound to _ is released at once (§5).mem::forget, process::exit and Rc cycles leak, safely. The promise is no undefined behavior, not tidy shutdown (§6).Box is just a pointer"Box drops its target and then frees the block (widget, Box). A raw pointer would end nothing.Checkpoint exercise
end of main. Then edit the program: swap the two field declarations in struct Pair and change let _ = Guard::new("ignored"); to let _ignored = Guard::new("ignored");, press run, and predict again before you step. Check against a scratch main.rs compiled by rustc. Answer: before the edits the four endings are drop first, drop second, drop new, drop kept. After both edits the fields end second then first, and ignored moves from a line right after its own make to the end of main, dropping last of all: reverse declaration order, with _ignored declared first.Where this points next
The rule is stated and the schedule derived: a value has one owner, the owner's end is the value's end, and the compiler writes the drop. Then one ordinary statement, let b = a;, put the rule under strain. An assignment copies a header, and a copied header is a second owner of the same buffer, the double free the rule was built to abolish. The rule cannot be dropped, because every safe program depends on it, and the copy cannot be forbidden, because programs must hand values from one name to another. So assignment itself must change. What must it mean for a value that owns something?
String, Vec and Box are fixed-size headers that own a buffer, and dropping the header releases it. A type's own Drop::drop runs first, then its fields in declaration order; locals end in reverse order; temporaries at the semicolon; drop(x) is an ordinary function that takes ownership. Leaks — mem::forget, process::exit — are safe, so a destructor can never carry a soundness argument. The one operation the rule strains is assignment: copying an owner's header makes a second owner.Interview prompts
- Why does Rust give every heap value exactly one owner, rather than one-or-more? (§1–§2 — two owners can each end the value: a double free or a use-after-free; zero owners leak it; one is the count for which the ending is well defined and checkable.)
- What is the difference between Rust's ownership and C++ RAII with
unique_ptr? (§1, §2 — same mechanism, scope end runs the free; in C++ a convention thatnew, raw pointers and copies can bypass, in Rust the only way to own a heap value in safe code, checked by the compiler.) - What exactly happens at the closing brace for a
String, aVecand aBox? (§3 — the header's slot ends and its drop runs: aStringfrees its buffer, aVecdrops its elements (today first to last; the docs do not promise it) and frees, aBoxdrops its target and then frees the block.) - In what order are values dropped, and why locals in reverse? (§5 — locals in reverse of declaration because a later value may depend on an earlier one; a struct's own
dropfirst and then its fields in declaration order, not literal order; temporaries at the end of their statement.) - Why can't you call
x.drop()yourself, and what doesdrop(x)do instead? (§4 — the compiler would dropxagain at the brace, breaking "one drop" (E0040);std::mem::dropis an empty function that takes ownership, so its parameter ends at its brace;drop(&x)does nothing, because a reference owns nothing.) - Does Rust guarantee no leaks? What does
mem::forgetbeing safe tell you? (§6 — no: leaks and skipped destructors cannot cause undefined behavior, so they are safe; therefore unsafe code must never rely on a destructor running.) - Why does a recursive struct need a
Box, and which alternatives does the compiler offer? (§3 — E0072: the size would be infinite; indirection makes it finite.Boxowns the next node,Rcshares it,&borrows it: three different ownership answers.) - What breaks if assignment simply copies an owner's bytes? (§7 — a copied
Stringheader is a second owner of one buffer, so scope end frees the block twice: the double free the ownership rule exists to remove.)
Companion reads: C++ · 04 The heap and the lifetime problem (the failure modes), C++ · 05 RAII (this rule as a discipline), C++ · 07 Ownership and smart pointers (unique_ptr, shared_ptr), and Go · 03 Pointers, the heap and the collector (the other answer).