all_lessons/Rust/03 · Ownership — one owner, one droplesson 4 / 23

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.

The thesis, here
Lesson 01 named the hazard: ending a place while another name can still reach it. The cheapest way never to end a place twice is to make the ending somebody's job and let only one somebody hold it. Ownership gives that job to the one kind of thing a program already ends exactly once, with nobody deciding: a slot at its closing brace.
Linear position
Forced by: On the stack every value has a scope: it is created at its declaration and ends at the closing brace, automatically, exactly once. But a String, a Vec, anything whose size is decided at run time, lives on the heap, where nothing ends it automatically. Who frees it, and how do we make sure it happens exactly once?
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?
The plan
Seven moves. (1) List the ways a heap value can be ended and price each against Lesson 00's budget. (2) Turn the survivor into a rule with two halves. (3) Look inside the owners the rule needs: 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 itHow it knows whenWhat it costs against Lesson 00's budgetWhat it leaves open
The programmerreads the code and writes free on every path outnothing at run time; unbounded thinking, because every new return is a new place to forgetall three hazards, invisibly
A collectora run-time component finds which values are still reachablea run-time system, which the budget rules out as the default; values end when it decides, not where the program's structure saysmemory only — a file or a lock is released by code you still write. (The Go track argues its case.)
A count on every valuethe last name to go ends ita counter stored with each value and updated on every copy and every end — a cost in every use, not only at the endtwo values that count each other never end
A name that already ends exactly onceits own end: a slot at its bracenothing beyond the free itselfthe 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.

Road not taken · count everything
Rust does not forbid the third row. When sharing is the design — several owners, the last to leave turns off the light — it is a library type you choose: 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:

The rule · one owner, one drop
Every value has exactly one owner: the place that holds it — a variable, a field of a struct, an element of a vector, the target of a box. When the owner ends, the value is dropped: its own ending code runs, the values it owns are dropped in turn, and the memory it owns is released.

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 youGo · constrains youRust · verifies
Who ends a heap valueyou, or a wrapper you chose to usethe collectorits owner, fixed at compile time
Whenat the delete, or the wrapper's destructorsome time after the last reference goesexactly where the owner's scope ends
What you can get wrongforget it, repeat it, use it aftertiming; non-memory resourcesthe 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...
}
Reading the error
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 endsOrderWhy it is that way
Locals of one scopereverse of declarationa later local may depend on an earlier one
Fields of a structdeclaration order — not the order written in a struct literalone fixed schedule per type, whoever constructs it
Elements of a tuple or arrayfirst to last (guaranteed); a Vec does the same today, but its documentation declines to promise itthe natural reading order
A temporaryat the end of the statement that made it, in reverse order of creationa 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 namean ordinary binding whose name merely starts with an underscore
Assigning x = newthe old value ends, after new is computedthe 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.

The drop machine
Pick a program, or edit it under Edit the program and press run. Each step is one thing the machine does: make a value, move a header, call drop, free a block, print. The stack shows one slot per name (dashed = ended or moved out); the heap shows the blocks, with an arrow from the owner. Drag the slider back before reading the readout, and predict the next line.
drops run
—
heap blocks live
—
blocks freed
—
does rustc accept it?
—
Edit the program
Show the core JS
function dropCell(cell, why, label) {
  if (halted || cell.st !== 'live') return;                          // moved out, or already ended: nothing to do
  var v = cell.v;
  if (!needsDropVal(v)) { cell.st = 'dropped'; return; }   // plain data has no ending to run
  var shown = label.charAt(0) === '⟨' ? label + ' = ' + shortVal(v).slice(0, 44) : label;
  ev('drop', 'drop ' + shown + (why ? '  (' + why + ')' : ''), { label: label, why: why });
  if (v.t === 'struct') {
    if (world.drops[v.name]) callFn(world.impls[v.name]['drop'], [{ t: 'ref', mut: true, cell: cell }], 'Drop::drop');   // 1. its own Drop::drop
    world.structs[v.name].fields.forEach(function (f) { dropCell(v.f[f.name], 'field', label + '.' + f.name); });          // 2. then its fields, in declaration order
  } else if (v.t === 'Vec') {
    v.items.forEach(function (c, i) { dropCell(c, 'element', label + '[' + i + ']'); });                                  // elements, first to last
    cell.st = 'dropped'; if (v.id) free(v.id, label);                                                                      // 3. then the memory it owns
  } else if (v.t === 'Box') { dropCell(v.inner, 'contents', '*' + label); cell.st = 'dropped'; free(v.id, label); }
  else if (v.t === 'String') { cell.st = 'dropped'; if (v.id) free(v.id, label); }
  else if (v.t === 'iterOwn') {
    for (var i = v.pos; i < v.items.length; i++) dropCell(v.items[i], 'unconsumed', label + '[' + i + ']');
    cell.st = 'dropped'; if (v.id) free(v.id, label);
  }
  cell.st = 'dropped';
}
function dropScope(sc) {
  for (var i = sc.locals.length - 1; i >= 0; i--) {
    var L = sc.locals[i];
    if (L.cell.st === 'live') dropCell(L.cell, 'scope end', L.name);
    else if (L.cell.st === 'moved' && L.cell.v && needsDropVal(L.cell.v)) { ev('skip', L.name + ' was moved: nothing to drop', { label: L.name }); }
  }
}

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.

What this rule does not cover
Ownership says who ends a value, not who may look at it in the meantime. A function that only reads a 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

"Rust frees memory when the last reference goes away"
A reference owns nothing: 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).
"Locals drop in the order I wrote them"
Reverse of declaration: 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).
"Destructors always run, so Rust cannot leak"
mem::forget, process::exit and Rc cycles leak, safely. The promise is no undefined behavior, not tidy shutdown (§6).
"A Box is just a pointer"
A pointer that owns: dropping the Box drops its target and then frees the block (widget, Box). A raw pointer would end nothing.

Checkpoint exercise

Try it
Load the schedule in the machine and, without running it, predict the last four lines printed after 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?

Takeaway
A heap value must be ended exactly once, and of four candidate enders — the programmer, a collector, a count on every value, a name that already ends once — only the last costs nothing at run time. So every value has exactly one owner, and the value is dropped when the owner ends; the compiler inserts the drop, so no line says "free". 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

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