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?
2. Why does `log(&Box::new(s))` compile when log takes `&str` and the argument is `&Box<String>`?
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?
4. A type implements Deref but deliberately not DerefMut (like Rc). What does that imply, and when is the DerefMut ladder required?
Flashcards
Question
What is the autoderef chain, and which operations use it?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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.