Skip to content
advanced Phase 11 · Smart Pointers and Interior Mutability

Deref Coercion

Let smart pointers transparently convert to their targets at call boundaries.

45m
0 problems
Topic Progress 0%

The Autoderef Chain: How a Box<String> Answers Every String Method

Ask a Box<String> for its length and it answers:

let b: Box<String> = Box::new(String::from("hello"));
assert_eq!(b.len(), 5);            // Box has no len(); String does
assert_eq!(b.as_bytes(), b"hello"); // and str has as_bytes; the chain keeps going

Nothing about Box declares len. What exists is one method — the Deref trait's single required function:

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

impl Deref for Box<T> { type Target = T; fn deref(&self) -> &T { /* std-internal */ } }   // schematic

Method resolution walks that implementation as a chain. When you write b.len(), the compiler tries the receiver in order: Box<String> (no), &Box<String> (no), &mut Box<String> (no), then applies deref once to reach String and tries again, then once more to reach str — where len() exists. Call it the autoderef ladder: each rung is one Deref::deref call, and the first rung with a matching method wins. Field access and indexing climb the same ladder, which is why Box::new((1, 2)).0 and b[0] on a boxed Vec compile without a single explicit *.

The ladder is what makes a smart pointer smart instead of merely annoying:

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

struct UserCache { hits: u32, store: std::collections::HashMap<String, String> }

impl Deref for UserCache {
    type Target = std::collections::HashMap<String, String>;
    fn deref(&self) -> &Self::Target { &self.store }
}

let cache = UserCache { hits: 0, store: Default::default() };
assert!(cache.get("key").is_none());   // HashMap methods reachable through the wrapper

One Deref impl gave the wrapper every HashMap method, and DerefMut (chapter 3) will give it every &mut method too. That is the legitimate use of the trait: a newtype that genuinely is its inner value for reading, or a pointer to one. The standard library's own docs warn against the illegitimate use — implementing Deref to simulate inheritance or to convert between unrelated types. For conversion, the explicit tools are From/Into, AsRef, and Borrow; Deref exists so that pointers behave transparently, not so APIs can hide their signatures.

Two clarifications keep the model precise. First, autoref happens at every rung: before dereferencing, the compiler tries the value itself, then &T, then &mut T — which is how boxed_vec.push(1) finds Vec::push(&mut self) through &mut Box<Vec<_>>. Second, the whole ladder is static: nothing runs, nothing allocates, and you can always spell a rung yourself when inference needs the help:

fn width(s: &str) -> usize { s.len() }

let b: Box<String> = Box::new(String::from("hello"));
width(&b);          // ladder + coercion: &Box<String> -> &String -> &str
width(&*b);         // one explicit deref: &String (then coerces)
width(&**b);        // two: &str directly — ugly, but it is the mechanism made visible

&*b is the manual rung, and it is exactly what you reach for when a generic signature defeats coercion (next chapter). Finally, know the ladder's cast of characters in std — the Target of each tells you what becomes reachable: Box<T>/Rc<T>/Arc<T> → T, Vec<T> → [T], String → str, PathBuf → Path, Cow<str> → str, Cell<T>/RefCell<T> → T. Half of Rust's "why does this method exist here?" questions are answered by one row of that list.

Interview framing: "Why does b.len() work?" is the standard opener for this topic. A complete answer names Deref, walks the ladder (Box → String → str), and adds that field access and indexing deref as well — then says where you should not implement it. The ladder is also the mechanism behind the conversions in the next chapter; the pointer itself comes from Box for Heap Allocation.

Why Your Function Takes &str and Still Accepts a &String

fn log(msg: &str) { println!("{msg}"); }

let owned = String::from("boot");
log(&owned);          // &String handed over, &str expected — compiles

No &owned[..], no .as_str(). The compiler converts &String into &str at the call, because String: Deref<Target = str> and the parameter's type is known. That automatic conversion is deref coercion, and it only fires where the compiler already knows what type it wants:

Coercion site Example Conversion applied
Function argument log(&owned) &String → &str
let with explicit type let s: &str = &owned; &String → &str
Struct field initializer Row { text: &owned } &String → &str
Return value fn name(s: &String) -> &str { s } &String → &str

Coercions also chain, because the compiler keeps dereferencing until the expected type appears — this is the part people underestimate:

fn size(s: &str) -> usize { s.len() }

let s = String::from("hello");
size(&s);                       // &String  -> &str          (one step)
size(&Box::new(s));             // &Box<String> -> &String -> &str   (two steps)
size(&Rc::new(String::from("x"))); // &Rc<String> -> &String -> &str  (via Deref for Rc)

Each intermediate type must implement Deref with the right Target, so the same rule explains &Vec<T> → &[T], &PathBuf → &Path, and &Box<dyn Error> → whatever it points at.

Where coercion does not happen is equally worth memorizing: wherever there is no expected type, there is no coercion. Generic parameters are the classic case:

fn count<T: AsRef<[u8]>>(data: T) -> usize { data.as_ref().len() }

let owned = vec![1_u8, 2, 3];
count(&owned);          // T is inferred as &Vec<u8>, NOT coerced to &[u8]

With a concrete fn f(x: &[u8]) the argument type is fixed, so coercion fires; with T the compiler infers whatever unifies — &Vec<u8> unifies fine, so no conversion is attempted, and code that later treats T as a slice can fail in a surprising place. The fix is always explicit: count(vec.as_slice()), count(&*owned), or a bound that describes what you mean (AsRef<[u8]>, as above).

Reading the failure is half the skill. When the expected type is known but no deref path reaches it, the compiler names the mismatch right at the site:

error[E0308]: mismatched types
  |
4 |     width(&data);
  |           ^^^^^^ expected `&str`, found `&Vec<u8>`

&Vec<u8> coerces to &[u8], never to &str — the ladder's Target chain decides which conversions exist, and no amount of expectation can invent a missing Deref impl. The other two coercion families share these sites but are not deref: unsized coercion (&[T; 3] → &[T], Box<Concrete> → Box<dyn Trait>) replaces the type's metadata instead of chasing pointers, and pointer-mutability coercion (&mut T → &T, allowed in that direction only) weakens exclusivity. All three fire only where an expected type exists; none survive into generic inference.

The other non-sites: operator operands and match scrutinees get no coercion (operators resolve through Trait impls instead — &String == &str works because of a PartialEq impl, not because of Deref), and method receivers are not coercion sites either — they use the autoderef ladder from chapter 1, which is a separate mechanism that happens to share its implementation.

Interview framing: expect to be asked both halves: "why does this compile?" (&String → &str at a known call site, chained through Deref) and "why doesn't this compile?" (a generic parameter, an operator, an unannotated let). Naming the expected-type requirement is the discriminator between someone who has read the rules and someone who has felt them.

DerefMut: The Same Ladder, Now With Write Access

Reads are only half the story. Mutation through a smart pointer goes through a second trait with the same shape:

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

DerefMut powers everything that needs &mut at the end of the ladder:

let mut b: Box<Vec<u32>> = Box::new(vec![1, 2]);
b.push(3);                       // DerefMut -> &mut Vec<u32> -> push

let mut s: Box<String> = Box::new(String::from("hi"));
let view: &mut str = &mut *s;    // explicit step...
let view2: &mut str = &mut s;    // ...or coercion at the site, same conversion

The second form is the mutable twin of &String → &str: at a site expecting &mut str, the compiler walks &mut Box<String> → &mut String → &mut str, requiring DerefMut at every rung — a read-only Deref impl is not enough for mutation, and a type that implements Deref without DerefMut therefore refuses mutation no matter how the pointer is used. (Rc does this deliberately: DerefMut must hand out &mut unconditionally, and with several handles "unconditionally" would mean several writers to one value.) The wrapper from chapter 1 completes itself the same way:

struct Counter { value: u32 }
impl std::ops::Deref for Counter {
    type Target = u32;
    fn deref(&self) -> &u32 { &self.value }
}
impl std::ops::DerefMut for Counter {
    fn deref_mut(&mut self) -> &mut u32 { &mut self.value }
}

let mut c = Counter { value: 0 };
*c += 1;               // place expression derefs mutably -> &mut u32 -> AddAssign
println!("{}", c);     // read path shows every u32 method too

Note the syntax: *c += 1 names the deref explicitly (operators themselves do not autoderef — c += 1 would demand AddAssign on Counter), while method calls and coercions insert the deref for you. The ladder hands out access; it does not relax aliasing. Two mutable steps through the same pointer still collide:

let a = &mut *b;
let c = &mut *b;    // error[E0499]: cannot borrow `*b` as mutable more than once

DerefMut grants the capability to take &mut T through the pointer — the borrow checker still decides how many such borrows may coexist, which is why the smart-pointer topics and the borrow-checker topics are the same story told from two sides.

Mutation also explains why indexing assigns: v[0] = 9 desugars through IndexMut, and the base expression autoderefs exactly like method receivers — boxed[0] = 9 on a Box<Vec<i32>> works for the same ladder reason boxed.push does. Similarly mem::swap(&mut *a, &mut *b) on boxed values is only spelling out the deref that coercion would insert.

Now the design rule, which is where interviews go deep: Deref makes an implicit promise. Implement it on a wrapper and every method of the target becomes visible on your type — callers can no longer see where your API ends and HashMap's begins. That is right for Box/Rc/Vec (the wrapper is a pointer; its Target is the whole point) and for newtypes whose identity is the inner value. It is wrong for "convenience conversions" between unrelated types — those belong to From, AsRef, or an explicit .inner() method, because implicit conversions hide cost and surprise callers. When you are asked "should my type implement Deref?", the answer is: only if it is a pointer to Target.

Interview framing: the full sequence an interviewer wants is: autoderef ladder (methods, fields, indexing) → coercion sites and the expected-type requirement → DerefMut for writes → the design warning about abusing Deref. With pointers now fully explained, the roadmap turns to what happens when you need several owners: Rc for Shared Ownership.

Quiz

1. `Box<String>` has no `len` method, yet `b.len()` compiles. What does the compiler actually do?

Question 1 options

2. Why does `log(&Box::new(s))` compile when log takes `&str` and the argument is `&Box<String>`?

Question 2 options

3. You write `fn all<T: AsRef<[u8]>>(d: T)` and call `all(&vec)` where `vec: Vec<u8>`. What type does T infer as, and why does no coercion intervene?

Question 3 options

4. A type implements Deref but deliberately not DerefMut (like Rc). What does that imply, and when is the DerefMut ladder required?

Question 4 options

Flashcards

Question

What is the autoderef chain, and which operations use it?

Answer

Method resolution tries the receiver type (and its &/&mut forms), then applies Deref::deref once and retries, repeatedly until a method matches — Box<String> to String to str for `b.len()`. Field access (Box::new((1,2)).0) and index expressions (boxed[0]) climb the same ladder. It is separate from deref coercion, which applies at typed coercion sites.

Question

What is deref coercion, and where does it apply?

Answer

An implicit &T → &U conversion when T: Deref<Target = U>, applied only where the compiler already knows the expected type: function arguments, `let` with an annotation, struct field initializers, and return values. It chains across multiple Deref steps (&Box<String> → &String → &str) and does NOT apply in generic arguments, operators, or unannotated positions.

Question

Why does `fn count<T>(...)` not get the coercion that `fn count(&[u8])` gets?

Answer

A concrete parameter type is an expected type, so coercion fires; a generic parameter has no expected type until inference runs, and inference unifies with whatever type the argument already is (&Vec<u8>). Coercion is driven by a known target, so in generic positions you convert explicitly with .as_str(), .as_slice(), or a bound like AsRef<str>.

Question

What does DerefMut add, and why can't an Rc be mutated through its pointer?

Answer

DerefMut: Deref supplies `fn deref_mut(&mut self) -> &mut Target`, enabling &mut coercions, mutating method calls (b.push), and index assignment — all read-only otherwise. Rc implements Deref but not DerefMut because DerefMut must hand out &mut unconditionally, and any other Rc handle could hold the same value — multiple writers through shared ownership. Mutation through shared Rc happens via RefCell (interior mutability), next in the roadmap.

Question

When should a type implement Deref, and what is the usual misuse?

Answer

Only when it is a pointer to Target (Box, Rc, Vec-backed wrappers) or a newtype whose identity IS the inner value — then transparent method access is the point. The misuse: implementing Deref to fake inheritance or convenience conversions between unrelated types; those belong to From/Into, AsRef, or an explicit method, because implicit conversion hides API boundaries and cost.

Revision Notes

Key Takeaways

  • 1. Deref's single method powers the autoderef ladder: method calls, field access, and indexing all climb Box → String → str one deref at a time until something matches
  • 2. Deref coercion converts &T to &T::Target only where an expected type exists — arguments, annotated lets, field initializers, returns — and it chains through several Deref steps
  • 3. Generic parameters are coercion black holes: inference unifies with the argument's actual type, so conversions must be written explicitly with as_str/as_ref
  • 4. DerefMut is the mutable rung: without it, mutation through the pointer is impossible in safe code — which is exactly how Rc forbids writes while keeping reads transparent
  • 5. Deref is for pointers and identity newtypes, not for faking inheritance; unrelated-type conversions belong to From/AsRef so API boundaries stay visible

Interview Tips

  • • "Why does b.len() compile on a Box<String>?" — walk the ladder (try receiver, deref, retry) and add that fields and indexing deref too; then volunteer the coercion-vs-autoderef distinction to show the mechanism, not just the effect
  • • "When does deref coercion NOT apply?" — anywhere without an expected type: generic arguments, operators, unannotated lets; the generic case is the one that bites in practice (map(|s| s.as_str()) vs hoping for coercion)
  • • "Should my wrapper implement Deref?" — only if it is a pointer to Target or its identity is the inner value; otherwise the implicit conversion hides the API boundary — reach for From/AsRef/an explicit method instead
  • • "Difference between Deref and DerefMut?" — DerefMut: Deref adds the &mut path; mutation, index assignment, and &mut coercions all require it, so a read-only Deref (Rc) makes the value immutable through the pointer in safe code

Cheat Sheet

Deref and Deref Coercion

The trait:

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

Two mechanisms, one implementation:

Mechanism Where Example
Autoderef (ladder) method calls, field access, indexing b.len(), Box::new((1,2)).0, boxed[0]
Deref coercion sites with a known expected type log(&owned), let s: &str = &owned, field init, return

Common conversions (all chain):
&String → &str · &Vec<T> → &[T] · &Box<String> → &str (two steps) · &Rc<T> → &T · &mut Box<String> → &mut str

No expected type → no coercion: generic arguments (T infers &Vec<u8>), operator operands (trait impls instead), unannotated let. Fix: explicit .as_str() / .as_ref() / .as_slice().

Rule of thumb: implement Deref only if your type is a pointer to Target. Conversions between unrelated types: From, AsRef, Borrow, or an explicit method. Deref without DerefMut (e.g. Rc) = read-through only, no mutation in safe code.

Continue Learning

Before You Start

Related Reading