Skip to content
← All articles

Deref is for smart pointers, and nothing else

Every type in this track — Box, Rc, Ref, RefMut — implements Deref, and that single trait is why they feel like the values they hold. It is also the most abused trait in Rust, and the rule for when to implement it is one sentence long.

You have spent this whole track calling methods on things that are not the values they appear to be.

let words: Rc<Vec<String>> = Rc::new(vec!["a".into()]);
words.len();          // Vec::len — but `words` is an Rc
words.iter().sum();   // Vec::iter

let boxed: Box<[u8]> = vec![1, 2, 3].into_boxed_slice();
boxed.len();          // <[u8]>::len

let cell = RefCell::new(vec![1, 2, 3]);
cell.borrow().first(); // slice::first, through a Ref guard, through a Vec

None of those methods exist on Rc, Box or Ref. One trait makes all of it work, and understanding exactly what it does — and exactly what it does not — is the difference between smart pointers feeling magical and feeling mechanical.

The trait

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

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

That is all of it: a function from &Self to &Target. Note that deref takes &self and returns &Target — it can only ever hand out a reference, never the value. And DerefMut requires Deref, so a type that can hand out &mut T can necessarily hand out &T too.

From that one method the compiler derives two behaviours.

The * operator. *x where x: Rc<T> desugars to *Deref::deref(&x). It produces a place of type T, not a value — which is why *rc cannot move the T out (E0507), and why *boxed can (a compiler special case for Box, and only Box).

Deref coercion. Wherever a &U is expected and you supply a &T with T: Deref<Target = U>, the compiler inserts the call for you. It applies to function arguments, to method receivers, and it repeats until it runs out of impls.

The chain

The repetition is where the power is. &Rc<String> walks:

&Rc<String>  --Rc: Deref<Target=String>-->  &String
&String      --String: Deref<Target=str>-->  &str

So a function taking &str accepts an &Rc<String> with no ceremony, and rc_string.trim() resolves to str::trim. &RefMut<'_, Vec<T>> walks to &Vec<T> and then to &[T], which is why guard.first() works on a RefCell<Vec<T>> borrow.

Method resolution uses the same chain, trying — for a receiver of type TT, then &T, then &mut T, then the deref target, and so on. That is why the syllabus’s data.to_vec() on an Rc<Vec<i32>> finds <[i32]>::to_vec two hops away.

💡Rc<T> implements Deref but not DerefMut. Why not, and what would break if it did? click to reveal

Because Rc cannot promise exclusivity, and &mut T is a promise of exclusivity.

DerefMut::deref_mut(&mut self) -> &mut Target would let you write *rc = value or rc.push(x) — and there is nothing stopping five other Rc handles to the same allocation from existing. Two of them could hand out &mut T simultaneously, which is exactly the aliasing-plus-mutation the entire language exists to prevent, and would be instant undefined behaviour rather than a mere logic bug.

That single missing impl is why the error you have seen so often reads the way it does:

error[E0596]: cannot borrow data in an `Rc` as mutable
  = help: trait `DerefMut` is required to modify through a dereference,
          but it is not implemented for `Rc<Vec<i64>>`

Box<T> does implement DerefMut, because a Box has exactly one owner and &mut Box<T> really is exclusive. RefMut<'_, T> implements it too — the runtime borrow flag is what makes the exclusivity claim true, and the guard’s Drop is what makes it temporary. Look down the list of DerefMut impls in this track and each one is a type that has some mechanism for proving there is only one writer.

The rule

Here is the whole guideline, from the standard library’s own documentation and from every style guide that has an opinion:

Implement Deref only for smart pointers. Do not implement it to get inheritance, or to save typing a field access.

The reason is that deref coercion is invisible at the call site. When it is moving between “a pointer to a thing” and “the thing”, that invisibility is a feature — Rc<Vec<T>> behaving like a Vec<T> is exactly what you wanted and nobody is confused. When it is moving between two different concepts, the same invisibility becomes a trap:

struct Inventory { items: Vec<Item> }

impl Deref for Inventory {          // don't
    type Target = Vec<Item>;
    fn deref(&self) -> &Vec<Item> { &self.items }
}

Now inventory.len() compiles. So does inventory.clear(), inventory.sort(), inventory.iter(), inventory.drain(..) and every other method Vec has or ever gains. You have not exposed an API; you have exposed a field, with no way to control which parts, no way to add an invariant, and no way to remove a method later without breaking callers. Add an inherent Inventory::len meaning something different and now readers have to know method-resolution order to tell which one runs.

Clippy calls this out by name with deref_polymorphism — Rust does not have inheritance, and simulating it here produces a type nobody can reason about. Write the two or three delegating methods you actually want. It is more typing and less magic, and that is the correct trade for a domain type.

The honest exception is the newtype wrapper: struct Meters(f64) or struct UserId(String), where the wrapper genuinely is the inner value plus a name. Even then, prefer AsRef, Borrow or an explicit .inner() unless the ergonomics really hurt — those are opt-in at the call site, which is the whole difference.

Five lints, and what each one is telling you

lint fires on why
explicit_deref_methods x.deref() Write &*x. Calling the trait method by hand is noise.
explicit_auto_deref &*x where coercion already applies The compiler was going to do it anyway.
borrowed_box &Box<T> in a signature &T is strictly more general and a &Box<T> coerces to it.
deref_addrof *&x Two operations that cancel out.
borrow_deref_ref &*x where x: &T Same. This one is usually a leftover from a refactor.

borrowed_box deserves a moment because it is the one that changes an API rather than tidying an expression. A parameter of type &Box<T> demands that the caller already has a Box. A parameter of type &T accepts a &T, a &Box<T>, a &Rc<T>, a &RefMut<T> and a plain field reference — all for free, all through the coercion this article is about. There is no situation where the first is better.

💡A function takes &Vec<String>. Clippy's ptr_arg says use &[String]. What has that got to do with Deref, and what does the caller gain? click to reveal

Everything, and quite a lot.

Vec<T>: Deref<Target = [T]>, so &Vec<String> coerces to &[String] automatically. That means the &[String] signature accepts everything the &Vec<String> signature accepted plus array references, slices of larger vectors, &Box<[String]>, and the result of &v[1..4]. The function loses nothing: with a &Vec you could not push or resize anyway, since you only had a shared reference.

This is the same shape as &String versus &str (String: Deref<Target = str>) and &Rc<Vec<T>> versus &[T]. The general rule that falls out: take the most-derefed type you can still do your job with. It costs the caller nothing and buys them the freedom to hold their data in whatever shape suits them.

Where the coercion stops

Two limits are worth knowing before they surprise you.

Coercion is for references, not for generics. A generic function fn f<T: Trait>(x: T) will not deref-coerce Rc<Concrete> into Concrete to satisfy T: Trait; trait bounds are matched on the type as written. Deref coercion happens during coercion sites — argument passing, method receivers, let with an explicit type — not during trait selection. This is the source of a lot of confusing “the trait is not implemented for Rc<Foo>“ errors, and the fix is usually &*rc or a where Rc<Foo>: Trait impl.

Coercion does not move. deref returns a reference, so no coercion can ever hand you an owned T. That is why let v: Vec<i64> = *rc; is E0507 and rc.to_vec() or (*rc).clone() is the answer.

What to carry forward

  • Deref is one method returning a reference, and everything else — the * operator, method resolution, argument coercion — is built on it.
  • Coercion repeats: &Rc<String> reaches &str in two hops, silently and for free.
  • DerefMut exists only for types that can prove exclusivity: Box, RefMut, MutexGuard. Rc cannot, so it does not have it, and that missing impl is the source of the E0596 you have seen all track.
  • Implement Deref only for smart pointers. For a domain type, write the delegating methods.
  • In signatures, take &str over &String, &[T] over &Vec<T>, and &T over &Box<T>. Deref coercion means it costs your callers nothing.