The Rust Lang Book: Smart Pointers and the Deref Trait
Smart pointers in Rust are specialized data structures that wrap raw data to provide additional metadata and capabilities, such as heap allocation or reference counting. To make these smart pointers as ergonomic to use as regular references, Rust provides the Deref trait. This chapter explores how the Deref trait allows you to customize the behavior of the dereference operator (*), enabling seamless transitions between smart pointers and the underlying values they hold.
1. The Problem: Why Do We Need the Deref Trait?
In Rust, smart pointers are implemented as custom structs. By default, the compiler does not know how to treat a custom struct like a regular reference. Without a dedicated mechanism, accessing the inner value of a smart pointer would require verbose, explicit method calls (e.g., my_pointer.get_inner_value()). This would ruin the ergonomics of the language, making smart pointers feel clunky and distinct from native references.
The Deref trait solves this problem. By implementing Deref, you can customize the behavior of the dereference operator (*). This allows you to write code that treats smart pointers exactly like regular references, keeping your codebase clean, readable, and idiomatic.
2. Core Mental Model: Pointers as Arrows
To understand dereferencing, it is helpful to use a simple mental model:
Real-Life Analogy
Think of a pointer as an physical arrow pointing to a value stored somewhere in memory. The pointer itself is just a memory address (the arrow), whereas the value it points to is the actual data (the target of the arrow).
When you use the dereference operator (*), you are telling the compiler to "follow the arrow" to retrieve the actual value stored at that memory address.
Dereferencing a Regular Reference
Consider the following example using a standard reference:
fn main() {
let x = 5;
let y = &x; // y is a reference (pointer) pointing to x
assert_eq!(5, x);
assert_eq!(5, *y); // Following the pointer y to its value
}
Visualizing the Reference Flow
y (Reference / Pointer)
|
| points to the memory address of x
v
x (Value: 5)
If you attempt to compare the pointer directly to the value without dereferencing it:
// This will fail to compile
assert_eq!(5, y);
The compiler will throw a type mismatch error because you are trying to compare an integer directly to a reference to an integer:
error[E0277]: can't compare `{integer}` with `&{integer}`
3. Naive vs. Better Approach: References vs. Smart Pointers
We can modify the previous example to use a smart pointer instead of a basic reference. Let's swap the reference &x with Box<T>, which is the most straightforward smart pointer in the standard library.
The Box Smart Pointer Approach
fn main() {
let x = 5;
let y = Box::new(x); // Box points to a copy of x on the heap
assert_eq!(5, x);
assert_eq!(5, *y); // Dereferencing the Box works exactly like a reference
}
Note on Copy Semantics: When the primitive integer
xis passed toBox::new(x), its value is copied onto the heap rather than its ownership being transferred. This is because integers implement theCopytrait.
Because Box<T> implements the Deref trait, the compiler allows us to use the * operator on y in the exact same way we did with a regular reference.
4. Building a Custom Smart Pointer: MyBox<T>
To understand how the Deref trait works under the hood, we can build our own simplified version of the Box<T> smart pointer, which we will call MyBox<T>.
We will define MyBox<T> as a tuple struct that holds a single generic value:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
Note: Unlike the standard library's
Box<T>, our customMyBox<T>stores its data directly on the stack rather than the heap. This is a deliberate simplification: our current focus is strictly on the behavior of the dereference operator, not on heap allocation mechanics.
The Compilation Failure
If we attempt to use our custom MyBox<T> in place of Box<T> inside our main function, the compiler will reject it:
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // ERROR!
}
When we compile this, we get the following error:
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
This error occurs because Rust does not know how to dereference our custom struct. To fix this, we must implement the Deref trait.
5. Implementing the Deref Trait
To enable dereferencing for MyBox<T>, we must bring the Deref trait into scope from the standard library and implement its required interface.
use std::ops::Deref;
// Implement Deref for our custom smart pointer
impl<T> Deref for MyBox<T> {
// Associated type defining the target type of the dereference
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0 // Return a reference to the inner value of the tuple struct
}
}
Breaking Down the Implementation:
type Target = T;: This is an associated type. It specifies the type that the dereference operator will yield. (Associated types are a way of declaring generic parameters for traits, which is covered in depth in Chapter 19 of the Rust Book).fn deref(&self) -> &Self::Target: Thederefmethod borrowsselfimmutably and returns an immutable reference to the inner data. BecauseMyBoxis a tuple struct,&self.0accesses the first (and only) element stored inside the struct.
With this implementation in place, our assertion in main now compiles and runs successfully:
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // This now compiles successfully!
}
6. Behind the Scenes: How Rust Desugars the Dereference Operator
Without the Deref trait, the Rust compiler only knows how to dereference native references (like &T). When we write *y on a type that implements Deref, the compiler automatically translates (desugars) the code behind the scenes.
Specifically, the expression:
*y
is converted by the compiler into:
*(y.deref())
The Desugaring Workflow
y (MyBox<T>)
|
| 1. Rust calls the deref() method: y.deref()
v
&T (A regular reference to the inner value)
|
| 2. Rust applies the built-in dereference operator (*)
v
T (The actual inner value)
Because Rust handles this conversion automatically, developers do not have to manually call .deref(). This allows us to write clean code while maintaining a clear distinction between smart pointers and regular references.
Why does deref return a reference instead of the value itself?
If the deref method returned the value T directly instead of a reference &T, Rust's ownership system would force the value to be moved out of the smart pointer. In the vast majority of cases, we want to access or inspect the inner value without taking ownership of it or emptying the smart pointer container. Returning a reference (&T) allows us to access the value safely without triggering a move.
7. Implicit Deref Coercion in Action
Deref Coercion is a highly convenient feature in Rust that happens automatically for types that implement the Deref trait. It automatically converts a reference to one type into a reference to another type when passed as an argument to a function or method.
The Code Example
Consider a function hello that expects a string slice (&str):
fn hello(name: &str) {
println!("Hello, {name}!");
}
If we wrap a String inside our custom MyBox smart pointer, we can pass a reference to MyBox<String> directly to the hello function:
fn main() {
// Create a MyBox containing a String
let m = MyBox::new(String::from("Rust"));
// Pass a reference to MyBox<String> to a function expecting &str
hello(&m);
}
This code compiles and runs perfectly, even though &m is of type &MyBox<String> and the function expects &str.
How the Coercion Chain Works
When the compiler encounters a type mismatch between the argument passed and the parameter expected, it analyzes the types to see if it can resolve the mismatch using Deref implementations. At compile time, Rust performs a chain of dereferences:
Because this resolution is resolved entirely at compile time, there is zero runtime performance cost associated with deref coercion.
8. The Pain of Manual Coercion
If Rust did not feature automatic deref coercion, developers would have to write highly verbose and complex code to achieve the same result.
To call the hello function with our MyBox<String> without deref coercion, we would have to write:
fn main() {
let m = MyBox::new(String::from("Rust"));
// Manual coercion without compiler assistance
hello(&(*m)[..]);
}
Deconstructing the Manual Syntax &(*m)[..]:
*m: Dereferences theMyBox<String>into the underlyingString.[..]: Takes a full string slice of theStringto yield the unsizedstrtype.&: Borrows the slice to produce the final expected&strreference.
This manual syntax is difficult to write, hard to read, and prone to visual clutter. Deref coercion makes this process completely transparent.
9. Deref Coercion Rules and Borrowing Constraints
Just as you use the Deref trait to override the * operator for immutable references, you can use the DerefMut trait to override the * operator for mutable references.
Rust performs automatic deref coercion in three specific scenarios based on the mutability of the source and target types:
Why Immutable to Mutable Coercion is Forbidden
Rust's strict borrowing rules state that you can have either any number of immutable references or exactly one mutable reference to a piece of data in a given scope, but never both.
If Rust allowed you to coerce an immutable reference (&T) into a mutable reference (&mut U), it would bypass this guarantee. The compiler cannot verify that the original immutable reference is the sole active reference to that data, which could lead to data races and undefined behavior. Therefore, converting an immutable reference to a mutable reference is strictly prohibited.
Summary and Key Takeaways
- The Purpose of
Deref: TheDereftrait allows you to customize the behavior of the dereference operator (*), enabling smart pointers to be treated like regular references. - The Pointer Analogy: Pointers can be conceptualized as arrows pointing to values in memory. Dereferencing is the act of "following the arrow" to get to the underlying value.
- The Desugaring Mechanism: When you write
*y, the Rust compiler automatically translates it to*(y.deref())behind the scenes. - No Ownership Transfer: The
derefmethod returns a reference (&T) rather than the value itself (T) to prevent ownership of the inner value from being moved out of the smart pointer. - Deref Coercion: This is a compile-time convenience feature that automatically converts a reference to one type into a reference to another type if they implement the
Dereftrait. - Zero Runtime Cost: Because deref coercion is resolved entirely by the compiler during compilation, it incurs no runtime performance penalty.
- Coercion Rules: Rust allows deref coercion from mutable to mutable, immutable to immutable, and mutable to immutable references. It strictly forbids coercing an immutable reference to a mutable reference to preserve memory safety and prevent data races.