Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 13 of 20

Medium Primitives

Drop order is specified

Build eight specified arrangements of values and let their destructors write the answer.

pub fn order_report(scenario: i64) -> Vec<String>

Why this is worth memorising

Deterministic destruction is Rust’s headline advantage over a garbage-collected language: you know when a file closes, a lock releases, a connection returns to the pool. But that is only an advantage if the order is knowable, and every RAII pattern you will ever write depends on these rules.

The rules, all of them:

Arrangement Order
let locals in a scope reverse declaration order
struct fields declaration order
tuple elements declaration order
array / Vec / slice elements front to back
a type with its own Drop impl its drop runs first, then its fields
let _ = expr; drops immediately, at the end of that statement
let _x = expr; a real binding — lives to the end of the scope
shadowed bindings both live to the end of the scope, then drop in reverse

Locals reverse and fields forward is genuinely non-obvious, and it is not arbitrary. Locals reverse because later declarations may depend on earlier ones — a guard declared after the thing it guards must be released before it. Fields go forward because a struct’s fields are constructed in declaration order and there is no dependency to invert.

The one that catches everyone is the last two rows of the middle group: let _ = f(); and let _x = f(); look like the same statement and have completely different lifetimes. _ is not an identifier; it is a pattern that matches and discards. Nothing owns the value, so it dies at the semicolon.

::: question A Vec<T> drops its elements front to back, but let locals drop in reverse. Isn’t that inconsistent? It looks inconsistent and it isn’t, because the two orders answer different questions.

Locals are a stack. You wrote them in an order, later ones may have been built out of earlier ones, and unwinding a stack means popping it — last in, first out. Reversing is the only order that never destroys something another local might still be holding a reference to.

A Vec‘s elements are peers. There is no “the third element was built out of the second”, so there is no dependency to respect, and front-to-back is simply the cheapest traversal: walk the buffer forward, drop each element, free the buffer once. Same for arrays and slices.

Struct fields sit in the peer category too, which is why they go forward. :::

The scaffolding

The grader injects these. Construction is silent — only destructors and your own markers appear in the log.

fn tracked(name: String, log: Log) -> Tracked   // silent
impl Drop for Tracked                           // pushes "drop <name>"
fn note(log: Log, line: String)                 // pushes an arbitrary line
fn new_log() -> Log
fn snapshot(log: Log) -> Vec<String>

struct Trio {
    first: Tracked,
    second: Tracked,
    third: Tracked,
    log: Log,
}
fn trio(a: String, b: String, c: String, log: Log) -> Trio
impl Drop for Trio                              // pushes "drop trio"

Log is a cheap handle; clone it freely with log.clone().

The eight scenarios

order_report(n) runs scene n and returns the log. Scene 0 is written for you as a worked example; build the rest.

0 — three locals. Locals named a, b, c, in that order. Nothing else.

1 — a struct. One local: trio("p", "q", "r", log).

2 — containers. In this order: a tuple of x and y; an array of p and q; a Vec of u and v. Build the Vec with Vec::from([...]) — clippy rejects a vec![] whose value is never used as one.

3 — an inner block. Local a; then a block containing local b and the marker "inside"; then the marker "after"; then local c.

4 — binding or not. Marker "start"; let _ = a value named imm; marker "mid"; let _held = a value named held; marker "end".

5 — shadowing. Two locals, both named _s, holding values named v and then w; then the marker "done".

6 and 7 — leaving early. Local a; then a block containing local b, the marker "inner", and — only in scene 6 — a return. If the function does not return there, it emits the marker "stayed", leaves the block, and emits "after".

Scene 6 is the interesting one: an early return from inside a nested block still destroys the inner block’s locals first and the outer ones second. Rust unwinds inside-out, and it does the same on a panic, which is what makes RAII guards sound in the presence of both.

Two things you cannot observe here

Closure captures. Do not build a test around the order in which a closure destroys its captured values. The Rust Reference deliberately leaves it unspecified, and code that depends on it is code that may break.

Temporaries. The scenes above avoid bare temporaries on purpose. The rules for when an unnamed intermediate value dies are subtle, and edition 2024 changed several of them (tail expressions and if let scrutinees in particular). They are worth learning — later, deliberately, and not by accident in the middle of a drop-order exercise.

Two clippy lints in this area are worth knowing by name even though both are allow-by-default: significant_drop_tightening (a lock guard held longer than it needs to be) and significant_drop_in_scrutinee (a guard kept alive for the whole of a match because it appeared in the scrutinee). Both are real bugs in real code, and both are drop-order bugs.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…