Rust Deref and DerefMut: Smart Pointer Behavior

• Deref and DerefMut enable transparent access to wrapped values, allowing smart pointers like `Box<T>` and `Rc<T>` to behave like regular references through automatic coercion

Key Insights

• Deref and DerefMut enable transparent access to wrapped values, allowing smart pointers like Box<T> and Rc<T> to behave like regular references through automatic coercion • Deref coercion chains multiple levels deep, letting &&String automatically convert to &str, but only works in specific contexts like function arguments and method calls • Deref should only implement pointer-like semantics—using it for general type conversions or inheritance-like behavior is an anti-pattern that violates the principle of least surprise

Introduction to Deref Coercion

Deref and DerefMut are the traits that make smart pointers feel natural in Rust. Without them, you’d constantly write (*box_value).method() instead of just box_value.method(). These traits implement the “transparent wrapper” concept: your smart pointer acts like it’s invisible, letting you work with the inner value directly.

When you call a method on a Box<String>, Rust doesn’t require explicit dereferencing. The compiler automatically inserts calls to deref() until it finds a type that has the method you’re calling. This is deref coercion, and it’s why smart pointers in Rust feel ergonomic rather than cumbersome.

fn main() {
    let boxed_string = Box::new(String::from("hello"));
    
    // These all work thanks to Deref coercion
    println!("Length: {}", boxed_string.len());
    println!("Uppercase: {}", boxed_string.to_uppercase());
    
    // Explicit dereferencing still works
    let s: &String = &*boxed_string;
    println!("Explicit: {}", s);
}

The magic happens because Box<T> implements Deref<Target = T>, telling the compiler it can treat &Box<T> as &T when needed.

The Deref Trait Mechanics

The Deref trait is deceptively simple:

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

The associated type Target specifies what type this smart pointer wraps. The deref() method returns a reference to that inner value. The ?Sized bound allows the target to be a dynamically sized type like str or [T].

Here’s how you’d implement your own smart pointer with Deref:

use std::ops::Deref;

struct MyBox<T> {
    value: T,
}

impl<T> MyBox<T> {
    fn new(value: T) -> MyBox<T> {
        MyBox { value }
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &T {
        &self.value
    }
}

fn main() {
    let boxed = MyBox::new(String::from("Rust"));
    
    // Deref coercion in action
    let len = boxed.len();  // Calls String::len()
    println!("Length: {}", len);
    
    // What actually happens under the hood:
    let len_explicit = (*boxed).len();
    println!("Explicit: {}", len_explicit);
    
    // Function call with coercion
    print_string(&boxed);  // &MyBox<String> → &String → &str
}

fn print_string(s: &str) {
    println!("String: {}", s);
}

When you write boxed.len(), Rust tries to find a len() method on MyBox<String>. It doesn’t exist, so Rust automatically calls deref() to get &String, then looks for len() there. This happens at compile time with zero runtime cost.

DerefMut for Mutable References

DerefMut extends Deref for mutable access:

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

Note that DerefMut requires Deref as a supertrait. You can’t implement DerefMut without also implementing Deref. This makes sense: if you can mutably dereference something, you should also be able to immutably dereference it.

use std::ops::{Deref, DerefMut};

struct MyBox<T> {
    value: T,
}

impl<T> MyBox<T> {
    fn new(value: T) -> MyBox<T> {
        MyBox { value }
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &T {
        &self.value
    }
}

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.value
    }
}

fn main() {
    let mut boxed = MyBox::new(vec![1, 2, 3]);
    
    // DerefMut allows mutable operations
    boxed.push(4);  // Calls Vec::push through DerefMut
    boxed[0] = 10;  // Mutable indexing
    
    println!("{:?}", *boxed);  // [10, 2, 3, 4]
    
    // Immutable operations use Deref
    let len = boxed.len();
    println!("Length: {}", len);
}

Rust’s borrowing rules apply normally: you can have many immutable dereferences or one mutable dereference, but not both simultaneously. DerefMut requires exclusive access to the smart pointer.

Deref Coercion Rules and Chains

Rust performs deref coercion in three scenarios:

  1. &T to &U when T: Deref<Target=U> — immutable to immutable
  2. &mut T to &mut U when T: DerefMut<Target=U> — mutable to mutable
  3. &mut T to &U when T: Deref<Target=U> — mutable to immutable

The third case is crucial: you can always coerce a mutable reference to an immutable one, but never the reverse. This preserves Rust’s safety guarantees.

Deref coercion can chain through multiple levels:

fn main() {
    let s = String::from("hello");
    let boxed = Box::new(s);
    let double_ref = &boxed;
    
    // This works: &&Box<String> → &Box<String> → &String → &str
    print_str(double_ref);
    
    // Multi-level mutable coercion
    let mut v = vec![1, 2, 3];
    let mut boxed_vec = Box::new(v);
    
    // &mut Box<Vec<i32>> → &mut Vec<i32> → &mut [i32]
    modify_slice(&mut boxed_vec);
    println!("{:?}", boxed_vec);
}

fn print_str(s: &str) {
    println!("{}", s);
}

fn modify_slice(slice: &mut [i32]) {
    if let Some(first) = slice.first_mut() {
        *first = 100;
    }
}

Each step follows the deref coercion rules. Box<String> derefs to String, which derefs to str. Rust performs as many dereferences as needed to make the types match.

Common Patterns with Standard Library Types

Understanding deref coercion unlocks powerful patterns with standard types.

String and &str: The most common example. String implements Deref<Target = str>, so any function accepting &str works with &String, &Box<String>, &Rc<String>, etc.

use std::rc::Rc;

fn process_text(text: &str) {
    println!("Processing: {}", text.to_uppercase());
}

fn main() {
    let owned = String::from("hello");
    let boxed = Box::new(String::from("world"));
    let rc = Rc::new(String::from("rust"));
    
    // All of these work seamlessly
    process_text(&owned);      // &String → &str
    process_text(&boxed);      // &Box<String> → &String → &str
    process_text(&rc);         // &Rc<String> → &String → &str
    process_text("literal");   // &str directly
}

Vec and slices: Vec<T> implements Deref<Target = [T]>, enabling slice operations on vectors.

fn sum_slice(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

fn main() {
    let vec = vec![1, 2, 3, 4, 5];
    let boxed_vec = Box::new(vec![10, 20, 30]);
    
    println!("Vec sum: {}", sum_slice(&vec));
    println!("Boxed sum: {}", sum_slice(&boxed_vec));
}

Smart pointers like Rc and Arc: These deref to their inner type, making shared ownership transparent.

use std::rc::Rc;

struct Config {
    name: String,
    value: i32,
}

fn use_config(cfg: &Config) {
    println!("{}: {}", cfg.name, cfg.value);
}

fn main() {
    let config = Rc::new(Config {
        name: String::from("max_connections"),
        value: 100,
    });
    
    // Rc<Config> derefs to Config automatically
    use_config(&config);
    
    // Can clone the Rc and still use it transparently
    let config2 = Rc::clone(&config);
    use_config(&config2);
}

Pitfalls and Best Practices

Don’t use Deref for type conversions. The biggest anti-pattern is implementing Deref to convert between unrelated types. Deref should only represent pointer-like semantics.

// ❌ ANTI-PATTERN: Using Deref for conversion
use std::ops::Deref;

struct Kilometers(f64);
struct Miles(f64);

// DON'T DO THIS
impl Deref for Kilometers {
    type Target = Miles;
    
    fn deref(&self) -> &Miles {
        // This is wrong on multiple levels
        // 1. Deref should return a reference to contained data
        // 2. This creates a temporary that can't be referenced safely
        unimplemented!()
    }
}

// ✅ CORRECT: Use From/Into for conversions
impl From<Kilometers> for Miles {
    fn from(km: Kilometers) -> Miles {
        Miles(km.0 * 0.621371)
    }
}

fn main() {
    let distance = Kilometers(10.0);
    let miles: Miles = distance.into();
}

Understand when coercion doesn’t happen. Deref coercion works in function arguments, method receivers, and a few other contexts, but not everywhere:

fn main() {
    let boxed = Box::new(5);
    
    // This works - function argument coercion
    takes_ref(&boxed);
    
    // This doesn't work - no coercion in generic type parameters
    // let vec: Vec<&i32> = vec![&boxed];  // Error!
    let vec: Vec<&i32> = vec![&*boxed];  // Explicit deref needed
}

fn takes_ref(x: &i32) {
    println!("{}", x);
}

Performance is usually not a concern. Deref coercion happens at compile time. The generated code is identical to manual dereferencing. However, be aware that calling deref() explicitly in a loop might look different to the optimizer than automatic coercion.

Avoid deref cycles. If type A derefs to B and B derefs to A, you’ve created an infinite loop that will cause compilation to hang or fail. The compiler has safeguards, but don’t rely on them.

Deref and DerefMut are powerful tools that make Rust’s smart pointers ergonomic. Use them to implement pointer-like types where the wrapper truly is transparent. Respect their intended semantics, and they’ll make your APIs feel natural to users.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.