Deref and DerefMut: Rust's Smart Pointer Protocol
By Yasir Shariff / / 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
Derefwasn’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;}Targetis the type that*valueevaluates to.dereftakes&selfand 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
Dereffor wrappers. It suits types that manage another value:Box<T>,Rc<T>,Stringoverstr,Vec<T>over[T]. ImplementingDerefon aUserstruct to reach itsnamefield imitates inheritance and confuses readers. Auser.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
AsReforBorrowfor conversions.Derefis for types where*valuemakes sense.AsRef<T>andBorrow<T>are general traits for cheap reference conversions;StringimplementsAsRef<str>, and so do types that aren’t pointers at all. - Be explicit when it reads better. If implicit coercions pile up, write
*valueor call a method likevalue.as_slice()to make the intent clear.
Further Reading
- The
Dereftrait in the standard library - Treating Smart Pointers Like Regular References with
Derefin The Rust Programming Language - Only smart pointers implement
Derefin the Rust API Guidelines - Understanding Pin in Rust: Move Semantics and Safety on this blog