Skip to content

← Smart Pointers and Interior Mutability step 24 of 26

Medium Primitives

Drop order in composite structures

Nine arrangements. Build each one exactly as specified and return the log its destructors write.

pub fn order_report(spec: Vec<String>) -> Vec<String>

spec names scenarios to run in order, against one shared log. An unrecognised name appends ? <name> and does nothing else.

A T logs drop <name> when dropped. A Bundle holds two Ts plus the log and logs drop Bundle[<first> <second>] in its own Drop. Both are given.

The scenarios, written out exactly. Reproduce them character for character — the whole point is that tiny differences change the answer.

fn locals(log: &Log) {                    fn fields(log: &Log) {
    let _a = T::new("a", log);                let _bundle = Bundle {
    let _b = T::new("b", log);                    first: T::new("f1", log),
    let _c = T::new("c", log);                    second: T::new("f2", log),
}                                                 log: Rc::clone(log),
                                              };
                                          }

fn tuple(log: &Log) {                     fn array(log: &Log) {
    let _t = (T::new("t0", log),              let _arr = [T::new("a0", log),
              T::new("t1", log),                          T::new("a1", log),
              T::new("t2", log));                         T::new("a2", log)];
}                                         }

fn block(log: &Log) {                     fn early_return(log: &Log, bail: bool) {
    let _outer = T::new("outer", log);        let _outer = T::new("outer", log);
    {                                         {
        let _inner = T::new("inner", log);        let _inner = T::new("inner", log);
    }                                             if bail {
    let _after = T::new("after", log);                 return;
}                                                  }
                                                  mark(log, "no-bail");
                                              }
fn temporary(log: &Log) {                     mark(log, "after-block");
    let _kept = T::new("kept", log);      }
    T::new("temp", log);
    mark(log, "mid");                     fn shadow(log: &Log) {
}                                             let _a = T::new("first", log);
                                              let _a = T::new("second", log);
                                              mark(log, "end");
                                          }

"early-return" calls early_return(log, true); "no-return" calls it with false.

The rules, and they are all specified

Unlike layout, drop order is part of the language. You can rely on every one of these:

construct order
local variables in a scope reverse of declaration
fields of a struct declaration order
elements of a tuple left to right
elements of an array or Vec index order, front to back
a value with its own Drop its drop runs first, then its fields
a temporary in an expression statement end of that statement
a shadowed binding still alive; drops with the rest, in reverse declaration order

Two of those trip almost everybody.

Locals go backwards, fields go forwards. It looks inconsistent and it is not. Locals are a stack: the last thing declared may depend on the earlier ones, so it must die first. Fields have no such dependency — they are declared together and destroyed in the order you wrote them.

A type’s own drop runs before its fields are dropped. That is why Bundle‘s destructor can still read self.first.name: the fields are alive and valid for the whole of Drop::drop, and only afterwards does the compiler recurse into them. If it were the other way round, every Drop impl would be reading freed memory.

And shadowing is not dropping. let _a = …; let _a = …; leaves two live values; the second name simply hides the first. Both die at the end of the scope, in reverse order, and the first one’s destructor runs last. If you expected shadowing to free the old value early, this is the case that corrects you.

The compile error in the starter

a.drop();
error[E0040]: explicit use of destructor method
  = help: consider using `drop` function: `drop(a)`

You may not call a destructor by hand. If you could, the value would then be dropped a second time when it went out of scope — a double free, in safe code. Drop::drop takes &mut self precisely so that it cannot consume the value; only the compiler is allowed to run it, exactly once.

std::mem::drop(a) is the sanctioned alternative, and it is famously the simplest function in the standard library — pub fn drop<T>(_x: T) {}. It takes the value by value, does nothing, and the value dies at the end of it. Ownership does all the work.

But read the second half of the starter’s comment before you reach for it: drop(a); drop(b); drop(c); would produce a, b, c — and this scenario asks for c, b, a. The right fix is to delete the calls entirely and let scope exit do its job.

Two notes on what is not here

Early return and panics still run destructors. early_return returns from inside a block with a live guard, and both inner and outer are dropped on the way out. The same holds for ?, for break, and for an unwinding panic. There is no path out of a scope that skips a destructor — which is the entire guarantee that makes RAII trustworthy.

No closures. You will notice none of the scenarios captures anything in a closure. That is deliberate: the relative drop order of a closure’s captured variables is unspecified by the Reference. Do not build anything that depends on it, and be suspicious of any tutorial that asserts one.

Loading visualization…