Deref and DerefMut let a smart pointer type transparently behave like
a reference to whatever it wraps. A type implementing Deref<Target = T>
can be used almost anywhere a &T is expected — most visibly, calling a
method defined on T directly on the smart pointer (my_box.method()
instead of (*my_box).method()), because the compiler automatically
inserts as many derefs as needed to find a matching method.
This coercion is what makes Box<T>, Rc<T>, String (whose
Deref::Target is str, so &String coerces to &str), and Vec<T>
(whose target is [T], so &Vec<T> coerces to &[T]) feel ergonomic to use day
to day — you rarely need to think about the wrapper layer at all, because
method calls, and reference coercions in function-argument position, see
straight through it to the underlying type.
The tradeoff worth knowing about: overusing custom Deref impls purely
to fake inheritance-like "is-a" relationships between unrelated types is
a recognized anti-pattern in Rust (sometimes called "Deref polymorphism")
— Deref is meant to model "acts like a reference to," not "is a kind
of," and stretching it to the latter tends to produce confusing method
resolution rather than genuinely reusable abstraction.
Basic usage example
fn greet(name: &str) {
println!("hello, {name}");
}
let boxed = Box::new(String::from("world"));
greet(&boxed); // <- &Box<String> coerces through Box then String to &str
Best practices & deeper information
Scenario
Designing a public API
A function that only needs to read a string should accept &str, not
&String — deref coercion means callers holding either a String or a
string literal can call it without extra ceremony.
fn greet(name: &str) { // <- PREFER: &str accepts both &String (via coercion) and &str directly
println!("hello, {name}");
}
// fn greet_narrow(name: &String) { ... } // AVOID: forces every caller to already own a String
let owned = String::from("Ada");
greet(&owned); // <- &String coerces to &str automatically
greet("Grace"); // a string literal is already &str; no coercion needed
Why this way:&str accepts everything &String does (deref
coercion covers that direction for free) plus string literals and other
&str-producing sources, so it's strictly the more widely callable
parameter type — the
Rust Book
recommends &str over &String as the more general parameter type for
exactly this reason.
Scenario
Writing generic code
A generic function bounded by AsRef<str> leans on the same "many
wrapper types act like a reference to the same target" idea deref
coercion embodies, letting one function body serve owned strings,
borrowed slices, and boxed strings alike.
fn shout<S: AsRef<str>>(value: S) -> String {
value.as_ref().to_uppercase() // <- works whether `value` is String, &str, or Box<str>
}
let boxed: Box<str> = "quiet".into();
println!("{}", shout(&*boxed)); // <- Box<str> derefs to str, coercing to &str at the call boundary
println!("{}", shout(String::from("also quiet")));
Why this way: writing generic code against AsRef<str> lets one
function serve owned strings, borrowed slices, and smart-pointer-wrapped
strings without the caller manually unwrapping anything — the
API Guidelines
recommend this for functions that only need read access to string-like
data.
Explanation
Embedded support: Full
Deref/DerefMut coercion is identical under #![no_std] — both traits
live in core::ops, and the compiler inserts the same automatic derefs
whether the target being deref'd to is str, [T], or something else
entirely. The mechanism shows up constantly in idiomatic no_std code
because heapless's fixed-capacity collections lean on it directly:
heapless::Vec<T, N> implements Deref<Target = [T]> the same way
std::Vec<T> does, so a heapless::Vec can be passed anywhere a plain
slice is expected, with no manual .as_slice() call needed. Driver crates
lean on it deliberately too: a thin newtype wrapper around a HAL
peripheral (struct Debounced<P>(P)) can implement Deref<Target = P> so
every method the wrapped peripheral exposes stays callable directly on the
wrapper — the wrapper adds behavior (debouncing, retries, a safety check)
without hand-writing a forwarding method for each one. As on a hosted
target, this is a distinct, legitimate use, separate from the "Deref
polymorphism" anti-pattern of stretching Deref to fake an inheritance
relationship between unrelated types — a debounce wrapper genuinely does
"act like" the peripheral it wraps, which is exactly what Deref is for.
Basic usage example
fn checksum(data: &[u8]) -> u8 {
data.iter().fold(0, |acc, b| acc ^ b)
}
let buf: heapless::Vec<u8, 16> = heapless::Vec::from_slice(&[1, 2, 3]).unwrap();
checksum(&buf); // <- &heapless::Vec<u8, 16> derefs to &[u8]
Best practices & deeper information
Scenario
Designing a public API
A function that only needs to read bytes should accept &[u8], not a
specific collection type — deref coercion means callers holding a
heapless::Vec can call it with no extra ceremony.
fn checksum(data: &[u8]) -> u8 { // <- PREFER: &[u8] accepts arrays, slices, and heapless::Vec alike
data.iter().fold(0, |acc, b| acc ^ b)
}
// fn checksum_narrow(data: &heapless::Vec<u8, 16>) -> u8 { ... } // AVOID: locks out arrays, other capacities, plain slices
let frame: heapless::Vec<u8, 16> = heapless::Vec::from_slice(&[0xAA, 0x01, 0x10]).unwrap();
checksum(&frame); // <- &heapless::Vec<u8, 16> coerces to &[u8]
checksum(&[0xAA, 0x01, 0x10]); // a plain array reference works too, no coercion needed
Why this way:&[u8] accepts everything a specific collection type
does (deref coercion covers heapless::Vec for free) plus arrays and
slices from any other source, making it the strictly more widely callable
parameter type — the same reasoning the
Rust Book
gives for preferring &str over &String applies directly to preferring
&[u8] over a concrete no_std collection type.
Scenario
Writing generic code
A thin wrapper around an embedded-hal I2C bus adds a retry policy without
needing to manually re-expose every method the underlying bus type has.
use core::ops::{Deref, DerefMut};
struct RetryingI2c<I2C>(I2C); // <- wraps a bus, adds retry behavior on top
impl<I2C> Deref for RetryingI2c<I2C> {
type Target = I2C;
fn deref(&self) -> &I2C { &self.0 }
}
impl<I2C> DerefMut for RetryingI2c<I2C> {
fn deref_mut(&mut self) -> &mut I2C { &mut self.0 }
}
fn read_register(bus: &mut RetryingI2c<impl embedded_hal::i2c::I2c>, addr: u8) {
bus.write(addr, &[0x00]).ok(); // <- calls I2c::write directly through the wrapper, no manual forwarding needed
}
Why this way: implementing Deref/DerefMut for a newtype wrapper is
the standard way to add behavior around a peripheral type without
hand-writing a forwarding method for every one the wrapped bus exposes —
this is Deref modeling "acts like a reference to the wrapped bus," the
legitimate use the classic explanation distinguishes from "Deref
polymorphism" fake inheritance.