skip to content
Yasir Shariff

Deref and DerefMut: Rust's Smart Pointer Protocol

By / / 4 min read

Updated:

String, Vec, and Box are smart pointers, yet you call methods on them as if they were the data they hold. my_string.len() works even though len is defined on str, not on String. When I first noticed this, it felt like a bit of Rust magic.

The mechanism is two traits, Deref and DerefMut, plus two compiler rules that use them: auto-deref for method calls and deref coercion for references. Together they let a wrapper behave like its contents without you writing * everywhere.

Personal Note: It clicked for me when I realized Deref wasn’t just about the * operator. It lets smart pointers act like the data they contain, especially in method calls, and once I saw that, much of the standard library suddenly made sense.

Call Methods Through a Smart Pointer

Deref customizes the dereference operator *. Implementing it tells Rust how to get a shared reference to the data your type wraps:

trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
  • Target is the type that *value evaluates to.
  • deref takes &self and returns &Self::Target.

When you call u.foo() and the type U has no method foo, Rust looks for foo on *u, the Target type, borrowing it as needed. If that type also implements Deref, the search continues another level, and so on. This is auto-deref.

String implements Deref<Target = str> and Vec<T> implements Deref<Target = [T]>, so str and slice methods are available on both:

fn main() {
let s = String::from("Hello, Rustaceans!");
// `len` and `starts_with` are defined on str
println!("Length: {}", s.len());
assert!(s.starts_with("Hello"));
let v = vec![1, 2, 3];
// `first` and `contains` are defined on [T]
assert_eq!(v.first(), Some(&1));
assert!(v.contains(&2));
}

Indexing with v[0] is a different mechanism: Vec<T> implements the Index trait directly.

Pass &String Where &str Is Expected

The second rule, deref coercion, applies when a reference has the wrong type. If U: Deref<Target = T>, Rust converts &U to &T wherever a &T is expected, repeating the step as many times as needed:

fn greet(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let owned = String::from("Ferris");
let boxed = Box::new(String::from("Corro"));
greet(&owned); // &String -> &str
greet(&boxed); // &Box<String> -> &String -> &str
}

This is why functions should usually take &str or &[T] rather than &String or &Vec<T>: callers holding any of the owning types can still pass them.

Mutate Through the Pointer with DerefMut

DerefMut is the mutable counterpart. It requires Deref, and returns &mut Self::Target:

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

It lets Box<T> hand out mutable access to the value it owns on the heap:

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

Build Your Own Smart Pointer

A minimal MyBox<T>, similar to Box<T> but without the heap allocation, shows both traits at work:

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
}
}
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
fn main() {
let mut smart_string = MyBox::new(String::from("Hello"));
// Deref: call a String method
println!("Length: {}", smart_string.len());
// DerefMut: modify the String
smart_string.push_str(", World!");
println!("{}", *smart_string); // Hello, World!
// Explicit dereferencing works too
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}");
}

deref returns a shared reference to the T inside MyBox, and deref_mut returns a mutable one. That is all it takes for String methods to work on a MyBox<String>.

Implement Deref Only for Pointer-Like Types

  • Keep Deref for wrappers. It suits types that manage another value: Box<T>, Rc<T>, String over str, Vec<T> over [T]. Implementing Deref on a User struct to reach its name field imitates inheritance and confuses readers. A user.name() method is clearer.
  • Remember coercion can hide what’s happening. If a method call doesn’t seem to belong to the type you expect, auto-deref may have found it a level down. An editor with rust-analyzer can show you where it resolved.
  • Use AsRef or Borrow for conversions. Deref is for types where *value makes sense. AsRef<T> and Borrow<T> are general traits for cheap reference conversions; String implements AsRef<str>, and so do types that aren’t pointers at all.
  • Be explicit when it reads better. If implicit coercions pile up, write *value or call a method like value.as_slice() to make the intent clear.

Further Reading