Rust Box, Deref and Drop¶
Rust's Box owns a heap-stored value; Deref enables pointer-like borrowed access; Drop supplies a destructor hook. These mechanisms interact, but neither dereferencing nor dropping a borrowed reference transfers ownership of its referent.
Prerequisites and outcome¶
Complete tests, documentation and Cargo. You will explain why recursive values need indirection, observe automatic and early destruction, and recognise limits of dereference coercion. Fixtures demonstrate ownership without opening files, accessing GPIO or measuring performance.
Build the complete Rust ownership example¶
Keep edition = "2024" in Cargo.toml and replace src/main.rs with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | |
Expected binary output:
There are seven tests. The binary output demonstrates local-variable drop order; separate marker tests assert that automatic and early destruction occur without relying on printed text. The custom Label hook is for observation only.
Box changes storage, not the ownership rules¶
Box
A List containing another List directly would have no finite recursive size. Box provides indirection of known size so each node can refer to another allocation. This tiny list is a language example, not a recommended replacement for Vec. Its recursive traversal and destruction can exhaust the stack for sufficiently deep input; allocating nodes on the heap does not remove that risk.
Box
The test moving String out of Box uses special built-in support for Box. A general user-defined Deref wrapper does not automatically gain permission to move a non-Copy target out through dereferencing.
Deref coercion borrows a target¶
Label's Deref implementation returns &str backed by its owned String. Calling byte_length(&label) borrows the target through coercion, and label.len() uses the target's method. Neither clones the String. Returning a reference to a temporary created inside deref would be invalid.
Deref's Target may be unsized. DerefMut is a separate trait for mutable dereferencing; implementing Deref alone does not grant mutable target access. Our Label has no DerefMut implementation, so it intentionally exposes only shared str behaviour. Read the Deref contract.
Use Deref only when transparent pointer-like behaviour is desirable and unsurprising. It is not a general-purpose inheritance mechanism or a substitute for every conversion API. Methods can collide with target methods, and implicit coercions become part of the public interface; explicit accessors are often clearer for domain objects.
Drop runs before the fields are destroyed¶
Our Label's destructor observes its String while that field is still valid. After the hook returns, Rust also destroys the fields; you do not manually free the String. Local variables are destroyed in reverse declaration order when their drop scope ends, which explains inner before outer. Struct fields have their own declaration-order rules, so do not generalise the local-variable rule to every aggregate. See destructors and drop scopes.
std::mem::drop(first) is a generic function consuming its argument. It makes early destruction explicit and first cannot be used afterward. Calling Drop::drop directly is forbidden; use the consuming function instead. Dropping a shared reference only discards that reference value, not its referent. Dropping a Copy value similarly destroys the passed copy, not every other copy. See std::mem::drop.
Drop has no Result return for reporting cleanup failure. For resources with fallible finalisation, provide an explicit operation where the caller can handle the error. Avoid panicking during destruction, particularly during unwinding. A type implementing Drop cannot also implement Copy. The Drop documentation explains the destructor contract.
Cleanup is not an unconditional execution guarantee¶
Normal scope exit and ordinary unwinding drop owned values according to their rules, but aborting, exiting the process or intentionally forgetting a value can bypass destructors. A destructor must not be the only basis for assuming a hardware action occurred, a file reached durable storage or an external transaction completed. This example does not open such resources.
Ownership, drop scopes and temporary lifetimes also affect when borrows end. Do not move fields out of a Drop type casually: its destructor may need the complete value. Use a deliberate consuming API, sometimes replacing a field with a valid empty value, when transferring one owned field is part of the design.
Deliberately failing: use after consuming drop¶
In a separate scratch project, replace src/main.rs with:
cargo check reports E0382. Moving label into drop ends this binding's ownership. Moving a clone instead would not destroy the original; it would allocate another String merely to destroy it.
Exercises and troubleshooting¶
- Replace std::mem::drop(first) with std::mem::drop(&first): expect a dropping_references warning, and first's destructor now runs at scope exit after second's. It is still valid to read first before the scope ends.
- Replace early drop with
Drop::drop(&mut first)and make its binding mutable: expect E0040. The hook cannot be invoked explicitly as an ordinary cleanup API. - Add a consuming method
fn into_string(self) -> String { self.0 }to Label: expect E0509 because Label implements Drop. A deliberate repair can take the String with std::mem::take on a mutable self, leaving an empty String for the destructor. - Replace List's Box
- field with List directly in a scratch type declaration: expect E0072 for infinite recursive size. Avoid editing the working constructors before inspecting the type error.
- Drop a Box
normally and explain which value and allocation it owns. Contrast that with dropping a &String reference: no transfer or destruction of the String's ownership occurs.
If coercion fails, inspect the target and mutability requirements. If a moved value is used afterward, inspect who now owns it. If cleanup timing matters, inspect actual drop scopes rather than adding a destructor solely to print a reassuring message.
Verification and next step¶
On October 10, 2026, the lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, all seven tests, formatting and debug/release output comparisons passed, including early and reverse-declaration drop order. Dropping a reference retained a usable owner, emitted the expected warning and deferred its hook to scope exit. The field-transfer repair passed an additional test. Use after drop, direct destructor calls, moving a field out of a Drop type and an unboxed recursive type produced E0382, E0040, E0509 and E0072 respectively. No hardware cleanup or performance claim is made.
Next, study Rc, Weak, Cell and RefCell to distinguish shared ownership from interior mutability.