skip to content
Yasir Shariff

The Magic of Deref and DerefMut: Rust's Smart Pointer Protocol

/ 6 min read

Ever found yourself using types like String, Vec, or Box in Rust and wondered how they seamlessly let you call methods as if they were the underlying data itself? I remember when I first encountered this. It felt like a bit of Rust magic – how could my_string.len() work when my_string was a String (a smart pointer) and not a &str directly?

The answer, my friends, lies in two powerful traits: Deref and DerefMut. These are the unsung heroes that make Rust’s smart pointers so ergonomic and intuitive. Let’s dive in and unravel this “magic” together!

The “Aha!” Moment with Smart Pointers

When I started with Rust, I knew about pointers from other languages. Usually, to get to the data a pointer points to, you need to explicitly dereference it, often with a * or an ->. Rust has this too, but it also has something more elegant for many common cases: Deref coercion.

Personal Note: My “aha!” moment came when I realized Deref wasn’t just about the * operator. It was a gateway, allowing smart pointers to act like the data they contained in many situations, especially method calls. It suddenly made so much of the standard library feel incredibly well-designed.

What is Deref? The Gateway to Inner Data

At its heart, the Deref trait allows you to customize the behavior of the dereference operator (*). When you implement Deref for your type, you’re essentially telling Rust, “Hey, when someone tries to dereference an instance of my type, here’s how to get an immutable reference to the inner data.”

The trait definition is surprisingly simple:

trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
  • Target: This associated type specifies what type *self will evaluate to.
  • deref(&self) -> &Self::Target: This method takes an immutable reference to self and returns an immutable reference to the Target type.

How Rust enables method call syntax on smart pointers:

This is where Deref coercion shines. If you have a type U that implements Deref<Target = T>, and you try to call a method foo() on an instance of U (u.foo()), Rust will do the following if U itself doesn’t have foo():

  1. Check if &T (the result of *u) has the method foo(). If yes, it calls (*u).foo().
  2. If not, and if T itself implements Deref, Rust will try to dereference T as well, and so on. This can happen multiple times.

This is why you can call &str methods on a String directly, or slice methods on a Vec<T>. String implements Deref<Target = str>, and Vec<T> implements Deref<Target = [T]>.

fn main() {
let s = String::from("Hello, Rustaceans!");
// String implements Deref<Target=str>
// So we can call &str methods directly on s
println!("Length: {}", s.len()); // Same as (*s).len() or (&s[..]).len()
assert!(s.starts_with("Hello"));
let v = vec![1, 2, 3];
// Vec<T> implements Deref<Target=[T]>
println!("First element: {}", v[0]); // Indexing is via Deref
assert_eq!(v.get(0), Some(&1));
}

And Then There Was DerefMut: For Mutable Access

What if you need to change the data a smart pointer holds? That’s where DerefMut comes in. It’s the mutable counterpart to Deref.

trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}

Notice that DerefMut itself requires Deref to be implemented. The deref_mut method takes a mutable reference to self and returns a mutable reference to the Target.

This allows smart pointers like Box<T> to give mutable access to the value they own on the heap.

fn main() {
let mut x = Box::new(String::from("initial"));
// Box<T> implements DerefMut<Target=T>
x.push_str(" content"); // (*x).push_str(" content")
println!("{}", x); // Prints "initial content"
}

Implementing Custom Smart Pointers

Let’s build a simple custom smart pointer, MyBox<T>, similar to Box<T>, to see Deref and DerefMut in action. Our MyBox<T> will simply wrap a value of type T.

use std::ops::{Deref, DerefMut};
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0 // Accessing the inner data
}
}
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 // Accessing the inner data mutably
}
}
fn main() {
let mut smart_string = MyBox::new(String::from("Hello"));
// Using Deref: Calling a String method
println!("Length: {}", smart_string.len()); // Compiles thanks to Deref!
// Using DerefMut: Modifying the String
smart_string.push_str(", World!"); // Compiles thanks to DerefMut!
println!("{}", *smart_string); // Prints "Hello, World!"
// Explicit dereferencing also works
let immutable_ref: &String = &*smart_string;
println!("Immutable ref: {}", immutable_ref);
let mutable_ref: &mut String = &mut *smart_string;
mutable_ref.make_ascii_uppercase();
println!("Uppercase: {}", mutable_ref);
}

In this MyBox<T> example:

  • deref returns an immutable reference to the T inside MyBox.
  • deref_mut returns a mutable reference to the T inside MyBox.

This allows us to call String methods directly on smart_string, even though smart_string is a MyBox<String>.

Common Pitfalls and Best Practices

  1. Overuse/Misuse of Deref: Deref should primarily be used for smart pointer types that “manage” another type. Don’t implement Deref just to get “inheritance-like” behavior or to provide a convenient alias if your type isn’t fundamentally a pointer or wrapper. This can make code confusing.

    • Good: Box<T>, Rc<T>, String (points to str), Vec<T> (points to [T]).
    • Maybe Not So Good: Implementing Deref for a User struct to get a &String for its name field. It’s usually clearer to provide a user.name() method.
  2. Deref Coercion Can Be Surprising: While powerful, sometimes Deref coercion can hide what’s actually happening. If you see a method call that doesn’t seem to belong to the type you think it is, remember that Deref coercion might be at play. Using an IDE with good Rust support can help trace these.

  3. Deref vs. AsRef/Borrow:

    • Deref is for types that are smart pointers. The * operator should make sense.
    • AsRef<T> and Borrow<T> are more general traits for providing a reference to an internal type T. They are often used for cheap reference-to-reference conversions. A type might implement AsRef<str> without being a smart pointer itself. For example, String implements AsRef<str>.
  4. Explicit is Sometimes Better: If the code becomes unclear due to too many implicit Deref coercions, don’t be afraid to use explicit dereferencing (*value) or method calls (value.as_slice()) to make the intent clearer.

Conclusion: The Unseen Convenience

Deref and DerefMut are cornerstone traits in Rust that provide immense ergonomic benefits, especially when working with smart pointers and collections. They allow types to seamlessly “act like” the data they wrap, reducing boilerplate and making code more intuitive.

Understanding how Deref coercion works demystifies a lot of Rust’s “magic” and empowers you to write more idiomatic and effective Rust code. So next time you call .len() on a String, give a little nod to Deref working diligently behind the scenes!

Happy (smart) pointing! 🦀✨