RustDeref & Drop Traits

Deref and Drop Traits

Two traits underpin how smart pointers work in Rust: Deref controls what happens when you write *pointer to dereference a value, and Drop controls what happens when a value goes out of scope. Together they make smart pointers feel like regular references while still performing their behind-the-scenes resource management.

The Deref Trait

Deref is what allows you to write *smart_pointer and reach the inner value. Any type that implements Deref can be used in many of the same places as a plain reference.

The trait requires one associated type and one method:

RUST
pub trait Deref {
    type Target;
    fn deref(&self) -> &Self::Target;
}

When you write *x, Rust silently rewrites it as *(x.deref()). The explicit * operator and the deref() method always go together — you never call deref() manually in normal code.

Implementing Deref on a Custom Type

Here is a minimal MyBox<T> that wraps a value and implements Deref so it behaves like the standard Box<T> when dereferenced.

RUST
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> Self {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0  // return a reference to the inner value
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);  // *y => *(y.deref()) => *(&5) => 5

    println!("x = {}, *y = {}", x, *y);
}
x = 5, *y = 5
Note
Rust inserts the call to deref() automatically whenever you write *y. You never invoke y.deref() directly in normal code — that is the compiler's job.
Deref Coercions

A deref coercion is an automatic, implicit conversion that Rust applies when a reference of one type is used where a reference of a different type is expected. The compiler chains .deref() calls as many times as needed to make the types match.

The three most common coercions:

From

To

Why it works

&String

&str

String implements Deref<Target = str>

&Vec<T>

&[T]

Vec<T> implements Deref<Target = [T]>

&Box<T>

&T

Box<T> implements Deref<Target = T>

RUST
fn print_str(s: &str) {
    println!("{}", s);
}

fn sum_slice(nums: &[i32]) -> i32 {
    nums.iter().sum()
}

fn main() {
    // &String coerces to &str automatically
    let s = String::from("hello, deref coercion!");
    print_str(&s); // no &s[..] needed

    // &Vec<i32> coerces to &[i32] automatically
    let v = vec![1, 2, 3, 4, 5];
    println!("sum: {}", sum_slice(&v));

    // &Box<String> coerces to &String, then to &str — two steps chained
    let boxed = Box::new(String::from("boxed string"));
    print_str(&boxed);
}
hello, deref coercion!
sum: 15
boxed string
Tip
Deref coercions mean you rarely need to write &s[..] to convert a &String to &str, or &v[..] for a slice. Just pass &s and &v — the compiler chains the coercions automatically.
DerefMut — Mutable Deref

DerefMut is the mutable counterpart to Deref. It enables *mut access and allows mutable deref coercions. Rust applies it when the source is &mut and the target is also expected as &mut.

RUST
use std::ops::{Deref, DerefMut};

struct Wrapper(String);

impl Deref for Wrapper {
    type Target = String;
    fn deref(&self) -> &String { &self.0 }
}

impl DerefMut for Wrapper {
    fn deref_mut(&mut self) -> &mut String { &mut self.0 }
}

fn append(s: &mut String) {
    s.push_str(" world");
}

fn main() {
    let mut w = Wrapper(String::from("hello"));
    append(&mut w); // &mut Wrapper coerces to &mut String via DerefMut
    println!("{}", *w); // "hello world"
}
hello world
The Drop Trait

Drop is how Rust implements RAII (Resource Acquisition Is Initialization). When a value goes out of scope, the compiler automatically inserts a call to its drop method. This is where file handles close, network connections disconnect, and allocated memory is freed — all without the programmer having to remember to do it.

The trait has one required method:

RUST
pub trait Drop {
    fn drop(&mut self);
}
Implementing Drop

RUST
struct Resource {
    name: String,
}

impl Resource {
    fn new(name: &str) -> Self {
        println!("acquiring: {}", name);
        Resource { name: name.to_string() }
    }
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("releasing: {}", self.name);
    }
}

fn main() {
    let _a = Resource::new("file handle");
    let _b = Resource::new("network connection");
    let _c = Resource::new("database lock");
    println!("--- all resources in use ---");
} // _c dropped first, _b second, _a last (LIFO order)
acquiring: file handle
acquiring: network connection
acquiring: database lock
--- all resources in use ---
releasing: database lock
releasing: network connection
releasing: file handle
Note
Variables are dropped in reverse order of creation — last in, first out (LIFO). This mirrors how a stack works and ensures that a value is never dropped before something it borrowed from.
Drop Order in Structs

Inside a struct, fields are dropped in the order they are declared — top to bottom. The struct's own drop method runs first, and then the fields are dropped.

RUST
struct Inner { label: &'static str }
struct Outer { first: Inner, second: Inner }

impl Drop for Inner {
    fn drop(&mut self) { println!("Inner '{}' dropped", self.label); }
}
impl Drop for Outer {
    fn drop(&mut self) { println!("Outer dropped (fields follow)"); }
}

fn main() {
    let _o = Outer {
        first:  Inner { label: "first" },
        second: Inner { label: "second" },
    };
    println!("using Outer");
}
using Outer
Outer dropped (fields follow)
Inner 'first' dropped
Inner 'second' dropped
Dropping a Value Early with std::mem::drop

Sometimes you want a value cleaned up before the end of its scope — for example, releasing a mutex lock early to reduce contention, or freeing a large buffer as soon as it is no longer needed.

You cannot call .drop() directly on a value — the compiler forbids it because doing so would cause a double-free (Rust would call drop once explicitly, then again automatically at end of scope). Instead use the free function drop(), which is in the prelude and requires no import.

RUST
struct Lock { name: String }

impl Drop for Lock {
    fn drop(&mut self) {
        println!("lock '{}' released", self.name);
    }
}

fn main() {
    let lock = Lock { name: String::from("mutex-1") };
    println!("lock acquired");

    // Release the lock early
    drop(lock); // correct — calls Drop and then invalidates 'lock'
    println!("lock released early, doing more work...");

    // 'lock' is no longer valid here
    // println!("{}", lock.name); // ERROR: use of moved value
}
lock acquired
lock 'mutex-1' released
lock released early, doing more work...
Warning
Never call .drop() directly on a value — the compiler rejects it to prevent a double-free. Always use the drop(value) free function, which moves the value in and drops it exactly once.
Copy and Drop Cannot Both Be Implemented

A type cannot implement both Copy and Drop. This is a deliberate constraint: Copy types are duplicated by a simple bitwise copy with no special semantics. If such a type also had Drop, dropping one copy could invalidate another copy's resources — a use-after-free.

If you implement Drop, the type automatically opts out of Copy.

RUST
// Combining Copy and Drop is a compile error:
// #[derive(Copy, Clone)]
// struct Handle { fd: i32 }
// impl Drop for Handle { fn drop(&mut self) { /* close fd */ } }

// The correct approach: only derive Clone (not Copy)
#[derive(Clone)]
struct Handle { fd: i32 }

impl Drop for Handle {
    fn drop(&mut self) {
        println!("closing fd {}", self.fd);
    }
}

fn main() {
    let h1 = Handle { fd: 3 };
    let h2 = h1.clone(); // explicit clone — not a silent bitwise copy
    println!("h1.fd = {}, h2.fd = {}", h1.fd, h2.fd);
} // h2 dropped, then h1 dropped
h1.fd = 3, h2.fd = 3
closing fd 3
closing fd 3
Real-World Uses of Drop
  • File handlesstd::fs::File closes the OS file descriptor in its drop implementation.

  • Network sockets — TCP streams flush pending data and close the connection when dropped.

  • Mutex guardsMutexGuard releases the lock when it goes out of scope, preventing deadlocks.

  • Database connections — connection pool handles return the connection to the pool in drop.

  • Custom allocators — memory arenas free their entire backing region when the arena struct is dropped.

Deref and Drop Together — A Complete Smart Pointer

Deref and Drop are exactly what makes Box<T>, Rc<T>, Arc<T>, and MutexGuard work. Here is a minimal smart pointer that combines both traits:

RUST
use std::ops::Deref;

struct SmartPtr<T> {
    data: Box<T>,
    label: &'static str,
}

impl<T> SmartPtr<T> {
    fn new(value: T, label: &'static str) -> Self {
        println!("[{}] allocated", label);
        SmartPtr { data: Box::new(value), label }
    }
}

impl<T> Deref for SmartPtr<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.data
    }
}

impl<T> Drop for SmartPtr<T> {
    fn drop(&mut self) {
        println!("[{}] freed", self.label);
    }
}

fn print_value(s: &str) {
    println!("value: {}", s);
}

fn main() {
    let ptr = SmartPtr::new(String::from("hello"), "ptr");
    // Deref coercion: &SmartPtr<String> -> &String -> &str
    print_value(&ptr);
} // Drop called automatically
[ptr] allocated
value: hello
[ptr] freed
Success
Deref makes smart pointers transparent — you use them just like ordinary references and the compiler inserts the indirection automatically. Drop guarantees cleanup without manual bookkeeping — resources are always released exactly once, in the correct order, at the correct time. These two traits together are the foundation of Rust's memory safety guarantees and the reason Rust can manage resources reliably without a garbage collector.