Skip to content

← Smart Pointers and Interior Mutability step 24 of 26

Medium Primitives

Drop order in composite structures

Build nine small ownership scenarios and return the messages their destructors write to a shared log.

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

Run the scenario names in spec in input order. Run repeated names again; do not deduplicate them. Names are case-sensitive and are not trimmed. An unknown name appends one entry containing "? " followed by the original name, unchanged. An empty input returns an empty log.

The starter provides T, Bundle, Log and mark:

  • Dropping a T appends drop <name>.
  • Bundle declares first, second and log in that order. Its own destructor appends drop Bundle[<first-name> <second-name>].
  • mark(log, text) appends text without a prefix.

Keep those helpers and implement the scenario functions below. Let their ownership and scopes produce the messages. The harness compares the returned log; it does not inspect how you implemented the scenarios.

The nine scenarios

In this table, creating a named value means calling T::new(name, log). Keep each value in a named binding such as _a until its stated scope ends. Do not insert explicit drop calls into these scenarios.

Name Actions, in order
locals In one function scope, bind a, then b, then c. Return normally.
fields Bind one Bundle with first named f1, second named f2, and log: Rc::clone(log). Return normally.
tuple Bind one tuple containing t0, t1, t2 in that order. Return normally.
array Bind one array containing a0, a1, a2 in that order. Return normally.
block Bind outer. Enter an inner block and bind inner. Leave that block, bind after, then return.
early-return Call early_return(log, true), defined below.
no-return Call early_return(log, false), defined below.
temporary Bind kept. Execute T::new("temp", log); as an expression statement without a binding. Mark mid, then return.
shadow Bind _a to a value named first. In the same scope, bind a new _a to a value named second. Mark end, then return.

Use this body for the shared early_return helper. It belongs alongside the starter's types and functions, rather than in a standalone program:

fn early_return(log: &Log, bail: bool) {
    let _outer = T::new("outer", log);
    {
        let _inner = T::new("inner", log);
        if bail {
            return;
        }
        mark(log, "no-bail");
    }
    mark(log, "after-block");
}

For example, spec = ["locals", "unknown"] produces ["drop c", "drop b", "drop a", "? unknown"]. Each scenario finishes before the next starts, and all write to the same log.

The drop rules used here

Value What happens when its scope or owner is dropped
Still-initialized local bindings Drop in reverse declaration order within their scope.
Struct fields Drop in declaration order.
Tuple fields Drop from first to last.
Array elements Drop from first to last.
A value implementing Drop Its Drop::drop runs before automatic destruction of its fields.

Bundle::drop can read its two T fields because their automatic destruction happens afterwards. The bundle message therefore precedes both field messages.

Arrays and vectors have different guarantees: Vec does not guarantee the order in which its elements are dropped. This exercise uses an array for the array scenario. Do not generalize its result to every collection.

In temporary, the unbound T is dropped at the end of its expression statement, before mark(log, "mid"). Other expression forms can have different temporary scopes, including lifetime extension in some bindings.

In shadow, the second declaration hides the first binding without moving or replacing its value. Both values remain to be dropped at function exit, with second before first. This is different from assigning a new value to an existing binding. Also, _a is a named binding; _ is a wildcard and does not keep a newly constructed value alive in the same way.

Fixing the starter's destructor calls

Calling a.drop() directly produces E0040, "explicit use of destructor method". Rust does not permit direct calls to Drop::drop. std::mem::drop(a) is a normal function that takes ownership, allowing the value to be destroyed when that function returns. Passing a reference to drop only drops the reference, not the value it refers to.

For locals, replacing the calls with drop(a); drop(b); drop(c); would produce the wrong order. Remove the calls and rename the unused bindings _a, _b and _c. Those names keep the values alive while avoiding unused-variable warnings.

This standalone example contrasts a kept binding with a temporary and an explicitly dropped value:

use std::cell::RefCell;

struct Trace<'a>(&'static str, &'a RefCell<Vec<&'static str>>);

impl Drop for Trace<'_> {
    fn drop(&mut self) {
        self.1.borrow_mut().push(self.0);
    }
}

fn main() {
    let log = RefCell::new(Vec::new());
    {
        let _kept = Trace("kept", &log);
        Trace("temporary", &log);
        let early = Trace("early", &log);
        drop(early);
        log.borrow_mut().push("end");
    }
    assert_eq!(*log.borrow(), ["temporary", "early", "end", "kept"]);
}

Scope exit and its limits

Normal return, early return, ? and break drop the still-owned values in scopes they leave. An unwinding panic also performs cleanup. This is the basis of RAII: owning a guard ties cleanup to that guard's lifetime.

Destruction is not guaranteed in every situation. An aborting panic or process exit can bypass cleanup, and mem::forget deliberately consumes a value without running its destructor. Leaked reference cycles can also prevent destruction. A moved value is now another owner's responsibility; its old binding must not drop it again.

The scenarios use normal control flow and no captured closures. Relative destruction order among a closure's owned captures is unspecified, so do not infer it from the rules for tuples or local bindings.