Skip to content

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

cargo new pi_resources
cd pi_resources

Keep edition = "2024" in Cargo.toml and replace src/main.rs with:

use std::ops::Deref;

enum List {
    Nil,
    Cons(i32, Box<List>),
}

impl List {
    fn len(&self) -> usize {
        match self {
            Self::Nil => 0,
            Self::Cons(_, tail) => 1 + tail.len(),
        }
    }

    fn sum(&self) -> i64 {
        match self {
            Self::Nil => 0,
            Self::Cons(value, tail) => i64::from(*value) + tail.sum(),
        }
    }
}

struct Label(String);

impl Deref for Label {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Drop for Label {
    fn drop(&mut self) {
        // A visible teaching hook, not production I/O cleanup.
        println!("drop={}", self.0);
    }
}

fn byte_length(label: &str) -> usize {
    label.len()
}

fn main() {
    let value = Box::new(46_700);
    println!("boxed={}", *value);

    let readings = List::Cons(46_700, Box::new(List::Cons(0, Box::new(List::Nil))));
    println!("list length={}, sum={}", readings.len(), readings.sum());

    let text: Box<str> = String::from("Pi 4B").into_boxed_str();
    println!("boxed text bytes={}", byte_length(&text));

    {
        let first = Label(String::from("first"));
        let second = Label(String::from("second"));
        println!("label bytes={}", byte_length(&first));
        std::mem::drop(first);
        println!("after early drop");
        println!("second bytes={}", second.len());
    }
    println!("after scope");

    {
        let _outer = Label(String::from("outer"));
        let _inner = Label(String::from("inner"));
        println!("both alive");
    }
    println!("finished");
}

#[cfg(test)]
mod tests {
    use super::{Label, List, byte_length};

    struct Marker<'a>(&'a mut bool);

    impl Drop for Marker<'_> {
        fn drop(&mut self) {
            *self.0 = true;
        }
    }

    #[test]
    fn box_can_transfer_an_owned_non_copy_value() {
        let boxed = Box::new(String::from("Pi 4B"));
        let owned: String = *boxed;
        assert_eq!(owned, "Pi 4B");
    }

    #[test]
    fn empty_recursive_value_has_zero_length_and_sum() {
        assert_eq!(List::Nil.len(), 0);
        assert_eq!(List::Nil.sum(), 0);
    }

    #[test]
    fn recursive_fixture_preserves_zero_and_negative_values() {
        let readings = List::Cons(-500, Box::new(List::Cons(0, Box::new(List::Nil))));
        assert_eq!(readings.len(), 2);
        assert_eq!(readings.sum(), -500);
    }

    #[test]
    fn deref_coercion_borrows_utf8_text() {
        let label = Label(String::from("π"));
        assert_eq!(byte_length(&label), 2);
        assert_eq!(label.len(), 2);
        assert_eq!(&*label, "π");
    }

    #[test]
    fn boxed_unsized_text_can_be_borrowed() {
        let label: Box<str> = String::from("Pi 4B").into_boxed_str();
        assert_eq!(byte_length(&label), 5);
    }

    #[test]
    fn leaving_scope_invokes_the_destructor() {
        let mut dropped = false;
        {
            let _marker = Marker(&mut dropped);
        }
        assert!(dropped);
    }

    #[test]
    fn consuming_drop_invokes_the_destructor_early() {
        let mut dropped = false;
        let marker = Marker(&mut dropped);
        std::mem::drop(marker);
        assert!(dropped);
    }
}
1
2
3
4
cargo check
cargo test
cargo fmt --check
cargo run --quiet

Expected binary output:

boxed=46700
list length=2, sum=46700
boxed text bytes=5
label bytes=5
drop=first
after early drop
second bytes=6
drop=second
after scope
both alive
drop=inner
drop=outer
finished

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 uniquely owns its contained value. Moving the box transfers that ownership; borrowing it does not. Destroying the box drops its contents and releases its allocation. Non-zero-sized examples here use heap allocation; zero-sized types have special allocation behaviour. See 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 owns dynamically sized text behind a pointer. That is distinct from &str borrowing text, and from a growable String. Rust knows the size of the owning pointer representation without requiring str itself to be Sized. Do not infer that every boxed type has one identical pointer layout or is freely interchangeable with a foreign pointer.

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:

1
2
3
4
5
fn main() {
    let label = String::from("Pi 4B");
    std::mem::drop(label);
    println!("{label}");
}

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Previous: tests and Cargo · Course overview

Donate