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.Stringis 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
}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
}
}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!
}What happened here?
- Rust sees that
greetwants&str. - We pass
&MiCaja. It callsderef-> we get&String. Stringalso implementsDeref(towardsstr). It callsderef-> we get&str.- 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:
BoxusesDropto free heap memory.RcusesDropto decrement the reference count.FileusesDropto close the file.MutexGuardusesDropto 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 hereOutput:
Pointers created.
Cleaning pointer with data: `D`
Cleaning pointer with data: `C`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 allowedRust 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");
}