rust-smart-pointers-deref-drop

Deref and Drop in Rust: foundation for safe smart pointers

  • 4 min

Deref and Drop are traits for dereferencing and cleaning up resources in types like smart pointers.

A Smart Pointer is usually a Struct.

  • Box<T> is a struct.
  • String is a struct.
  • Rc<T> is a struct.

Normally, you cannot dereference a struct (*my_struct would error). All values are destroyed when they go out of scope, and their fields are also destroyed; to add custom cleanup behavior, we implement Drop.

The Deref trait

The Deref trait allows customizing the behavior of the dereference operator *.

Without Deref, if we create our own smart pointer, we would have to access the inner value manually every time.

Implementing our own Box

Let’s create a simple struct called MiCaja to see the problem.

struct MiCaja<T>(T); // Generic tuple struct

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

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

    assert_eq!(5, x);
    // assert_eq!(5, *y); // ❌ Error: type `MiCaja<{integer}>` cannot be dereferenced
}
Copied!

Rust doesn’t know how to apply * to MiCaja. To fix it, we implement Deref:

use std::ops::Deref;

impl<T> Deref for MiCaja<T> {
    type Target = T; // We define what type is inside

    fn deref(&self) -> &Self::Target {
        &self.0 // Return a reference to the first element of the tuple
    }
}
Copied!

Now, when we write *y, Rust actually executes behind the scenes: *(y.deref())

It gets the inner reference and then dereferences it. Now our struct behaves like a pointer!

Deref Coercion

Thanks to Deref, Rust offers an incredible quality-of-life feature called Deref Coercion.

Rust can automatically convert a reference to a smart pointer into a reference to its content. And it does so in a chain.

Imagine a function that expects a string slice (&str):

fn greet(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MiCaja::new(String::from("Rust"));

    // Call the function passing &MiCaja<String>
    greet(&m); // ✅ It works!
}
Copied!

What happened here?

  1. Rust sees that greet wants &str.
  2. We pass &MiCaja. It calls deref -> we get &String.
  3. String also implements Deref (towards str). It calls deref -> we get &str.
  4. It matches!

Without this feature, we would have had to write: greet(&(*m)[..]). Thanks Deref.

The Drop trait

The second pillar is Drop. This trait allows us to execute code when a value is about to go out of scope. It’s the equivalent of a Destructor in C++.

It’s important for Smart Pointers:

  • Box uses Drop to free heap memory.
  • Rc uses Drop to decrement the reference count.
  • File uses Drop to close the file.
  • MutexGuard uses Drop to release the lock.

Implementing Drop

Let’s create a noisy struct that notifies us when it dies.

struct NoisyPointer {
    data: String,
}

impl Drop for NoisyPointer {
    fn drop(&mut self) {
        println!("Cleaning pointer with data: `{}`", self.data);
    }
}

fn main() {
    let c = NoisyPointer { data: String::from("C") };
    let d = NoisyPointer { data: String::from("D") };

    println!("Pointers created.");
} // Scope ends here
Copied!

Output:

Pointers created.
Cleaning pointer with data: `D`
Cleaning pointer with data: `C`
Copied!

Notice the order: local variables are destroyed in reverse order of their declaration. That’s why d is destroyed before c, regardless of how the compiler decides to physically store these values.

Forcing cleanup (std::mem::drop)

Sometimes you want to release something before the scope ends (for example, to release a Lock or close a file so another process can use it).

You cannot call the drop method manually:

// c.drop(); // ❌ Explicit error: Explicit destructor not allowed
Copied!

Rust prohibits this to avoid the “Double Free” error (trying to free memory twice: once manually and once automatically at the end of scope).

If you want to force it, use the standard library function:

fn main() {
    let c = NoisyPointer { data: String::from("C") };

    println!("Before drop");
    std::mem::drop(c); // Force cleanup here
    println!("After drop");
}
Copied!