Implementing Deref for Custom Types

Learn how to implement the Deref trait on custom types to enable dereferencing with the * operator and prepare them for deref coercion

The * operator in Rust follows a pointer to the value it points to. With references like &i32, the compiler knows exactly what to do: read the reference, hop to the referenced memory, and hand back the i32. But when you build your own type that owns some data—like a smart pointer—the compiler has no idea how to reach that data. You have to teach it by implementing the Deref trait.

That is the whole purpose of Deref: to tell the compiler, "if someone uses * on my type, run the logic in my deref method and then dereference whatever it returns." Once you do that, your custom type behaves like a reference in many contexts, including deref coercions that will be covered in the next section.

What the Deref Trait Looks Like

The trait lives in std::ops::Deref and its definition is deliberately minimal:

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

Two pieces matter here. The associated type Target names the type you get after a successful dereference—usually the inner value you are wrapping. The deref method takes &self and returns a shared reference to that Target.

DerefMut:

If you need mutable access through *, implement DerefMut as well. Its deref_mut method returns &mut Self::Target. Everything described here about Deref applies to DerefMut in a mutable context, but the same design rules and pitfalls carry over.

Building a Custom Smart Pointer

The easiest way to see Deref in action is to build a type that mimics Box<T>. We will wrap a value in a tuple struct and then teach Rust how to reach inside it.

use std::ops::Deref;
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
    }
}
fn main() {
    let x = 5;
    let y = MyBox::new(x);
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

MyBox owns the value inside its single field. The Deref implementation declares that the target of dereferencing is T itself—whatever type the caller placed inside. The deref method simply returns a reference to that inner value through &self.0.

When the compiler sees *y, it transforms the expression roughly into *(y.deref()). It calls deref to get a &i32 and then applies the built‑in dereference operation on that reference to obtain the i32. The assertion passes because 5 and the dereferenced value are the same integer.

Custom Dereferencing Works:

If your output compiles without errors and all assertions pass, you have successfully implemented Deref for your own type. Your MyBox now behaves like a reference in any situation that uses the * operator.

Why deref Returns a Reference

A question often asked is: why not return the owned value directly? The answer is ownership. If deref returned T instead of &T, the value would be moved out of self. That would consume the smart pointer and leave the original variable unusable, which is almost never desired. Returning a reference allows the caller to inspect the data without taking ownership, keeping the original variable intact. The final plain dereference (*) operates on that reference, which is always safe because references are copyable and never own the data.

The same logic applies to DerefMut: it returns a mutable reference, so the inner value stays inside the smart pointer while being modifiable in place.

Compiler Substitution in Detail

When you write *some_value, the compiler does not generate arbitrary code. It follows a specific pattern:

  1. If some_value is a reference or raw pointer, it dereferences directly—this is built into the language.
  2. Otherwise, it looks for a Deref implementation. It replaces *some_value with *Deref::deref(&some_value).
  3. The call to deref produces a reference. The outer * then dereferences that reference to give the final value.

That outer dereference is always a simple pointer dereference. It never triggers another deref call. This is why Rust can handle layers of smart pointers without infinite recursion: each * triggers exactly one deref method call, and the result is a reference that Rust knows how to follow natively.

A Glimpse of Deref Coercion

Implementing Deref does more than enable the * operator. It also unlocks deref coercion, where the compiler automatically inserts calls to deref to make types match. The next section covers coercion in depth, but a small demonstration shows how the foundation you just built matters.

use std::ops::Deref;
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) -> &T {
        &self.0
    }
}
fn hello(name: &str) {
    println!("Hello, {}!", name);
}
fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);  // &MyBox<String> coerces to &str
}

The function hello expects a &str. We have a &MyBox<String>. Because String implements Deref<Target = str>, the compiler can chain two deref calls—MyBox<String>Stringstr—to obtain a &str. This is a preview of the compiler convenience that Deref enables. You write a reference to the smart pointer, and the compiler does the conversion work.

When You Should Implement Deref

The Deref trait is not a general-purpose conversion tool. Its implicit nature means it becomes part of your type’s public API in ways that are hard to reverse. The Rust standard library documentation gives clear guidance.

Implement Deref (and DerefMut) only when your type transparently behaves like a pointer to the target type. This includes cases where:

  • The purpose of the type is to change ownership semantics (e.g., Rc<T>, Arc<T>).
  • The purpose is to change storage semantics (e.g., Box<T>, Vec<T>).
  • The deref operation is infallible and cheap—ideally just returning a reference to an existing field.

If your type enforces invariants on the inner data and exposing it directly would break those invariants, Deref is the wrong choice. Use explicit conversion methods like as_ref or as_str instead.

deref Must Not Fail Unexpectedly:

Because Deref::deref can be called implicitly by the compiler during deref coercion, a panic or an error return inside deref would appear in places the programmer never wrote a *. That makes debugging extremely difficult. The method may panic on programmer error (e.g., an internal state violation that should never happen), but it must never fail due to unpredictable runtime conditions like I/O or invalid user input.

Common Mistakes When Implementing Deref

The most consequential mistake is treating Deref as a shortcut to inherit an interface. A type that implements Deref automatically exposes all methods of the target type that take &self. This method forwarding can cause collisions and surprising behavior.

Consider a hypothetical AsciiString wrapper that ensures the inner Vec<u8> contains only ASCII bytes. If you implement Deref<Target = str> so that AsciiString can be used like a string slice, you also inherit every method from str. You might later implement Index<Range<f64>> for a custom indexing feature, only to discover that the compiler can no longer find Index<Range<usize>>—which was inherited from str—because method resolution picks your f64 implementation instead. The logic that chooses which method to call searches the receiver type and its deref targets, and naming collisions are resolved in ways that are not always obvious.

use std::ops::{Deref, Index};
struct AsciiString(Vec<u8>);
impl Deref for AsciiString {
    type Target = str;
    fn deref(&self) -> &str {
        // ... convert bytes to &str ...
        "placeholder"
    }
}
impl Index<std::ops::Range<f64>> for AsciiString {
    type Output = str;
    fn index(&self, _: std::ops::Range<f64>) -> &str {
        ""
    }
}
fn main() {
    let a = AsciiString(vec![65, 66, 67]);
    // The following line now fails to compile because the compiler
    // expects Range<f64> instead of Range<usize>.
    // let slice = &a[1..3];
}

The compiler sees AsciiString, dereferences it to str, and finds Index<Range<usize>> from str. But it also finds your Index<Range<f64>> on AsciiString directly. Because AsciiString has its own Index impl, the search stops there and never considers the str implementation. The takeaway: Deref is not a mechanism for composition; it is a mechanism for pointer-like delegation.

Beware Method Collisions:

Any method you define on your type can shadow a method that would otherwise be available through deref coercion. This can lead to compile‑time errors or, worse, silently different behavior if the signatures are compatible. Use Deref only when you are certain that your type should behave as if it is the target type in all contexts where a reference is taken.

Summary

Implementing Deref for a custom type is how you give that type the ability to be dereferenced with *. The trait asks for a target type and a single method that returns a reference to that target. Once implemented, *my_value becomes a call to deref followed by a plain pointer dereference. This unlocks not only explicit dereferencing but also deref coercion, which allows smart pointers to be used in places that expect references.

The most important insight: Deref is an opt‑in promise that your type behaves like a reference to Target. It should be used sparingly—only when that promise holds—and its deref method must be cheap and infallible. For conversions that do not fit that contract, explicit methods and the AsRef or Borrow traits are the safer path.