Skip to content

← Generics and Traits step 13 of 24

Medium Primitives

Deref polymorphism, and the day your method disappears

Programmers arriving from an object-oriented language discover Deref, notice that it makes a wrapper “inherit” every method of the wrapped type, and build a class hierarchy out of it. It compiles. It feels wonderful for about a week. Then somebody adds a method and nobody can explain what the code does any more.

This problem is that week, compressed.

Your task

pub struct Inventory { slots: Vec<String> }
impl Deref for Inventory { type Target = Vec<String>; ... }

impl Inventory {
    pub fn len(&self) -> usize      // counts NON-EMPTY slots
    pub fn is_empty(&self) -> bool
}

pub trait Weight { fn total(&self) -> usize; }
pub trait Bulk   { fn total(&self) -> usize; }

pub fn counts(slots: Vec<String>) -> Vec<usize>

counts must return four numbers, in this order:

  1. the inventory’s own len — how many slots are non-empty
  2. the underlying Vec‘s len — how many slots exist at all
  3. Weight‘s total — the sum of the slot strings’ byte lengths
  4. Bulk‘s total — ten times the raw slot count

Everything except the body of counts is already written. All you have to do is call four methods, and that turns out to be the hard part.

Problem one: your inherent method silently wins

Inventory::len and Vec::len have the same name. Write inv.len() and you get the inherent one — always. There is no ambiguity error, no warning, nothing at the call site to hint that a second len exists. Method resolution builds the deref chain Inventory → Vec<String> → [String] and takes the first match, and inherent methods always win over anything found deeper.

To reach Vec::len you must deref explicitly first:

(*inv).len()      // deref, THEN resolve — Vec::len
Vec::len(&inv)    // or name the type and let the argument coerce

Both work. Neither is something you would guess. And notice the direction of the damage: it is not that your code fails to compile — it is that inv.len(), which every reader will assume means “how many things are in here”, means something you invented.

Problem two: two traits, one name

Weight and Bulk both declare fn total(&self). Both are implemented for Inventory. Write inv.total() and:

error[E0034]: multiple applicable items in scope
   |
55 |         inv.total(),
   |             ^^^^^ multiple `total` found
   |
note: candidate #1 is defined in an impl of the trait `Bulk` for the type `Inventory`
note: candidate #2 is defined in an impl of the trait `Weight` for the type `Inventory`
help: disambiguate the method for candidate #2
   |
55 -         inv.total(),
55 +         Weight::total(&inv),

E0034 is the honest version of problem one. Here the compiler cannot pick, so it stops and shows you both candidates. In the len case it could pick, so it did — quietly. Same underlying algorithm, wildly different experience.

The fix is fully-qualified syntax: Weight::total(&inv). The most explicit form, which always works, is <Inventory as Weight>::total(&inv); you need that one when even the trait name is ambiguous.

Why std says don’t do this

The standard library’s position is that Deref is for smart pointers, and the reason is exactly what you just did: implementing Deref injects your type into everybody’s method-resolution chain. Every method the target ever gains is now a method your type appears to have, and every method you add is a potential silent shadow of one of theirs.

There is no Deref-based way to say “inherit these five methods but not those three”. It is all or nothing, forever, including methods that do not exist yet.

DerefMut doubles the surface. It is the &mut self half of the same trait, and it is what lets vec[0] = x work through a wrapper. Everything above applies to it, plus the autoref rule that a method lookup tries &self before &mut self at each step, so a &self method on your type shadows a &mut self method on the target.

The honest counterpoint

Real crates are full of newtype wrappers that deref to the inner type, and they are fine. The rule is a strong default, not an absolute. What makes the Inventory above bad is specific and checkable:

  • it derefs to a collection, not to a pointee it conceptually is;
  • it defines a method that collides with one on the target;
  • the two lens disagree, so the shadowing changes behaviour rather than just routing it.

Break none of those and a deref newtype is a reasonable design. Break the third one and you have built a trap for your future self.

Finally, keep the three-way distinction straight, because learners collapse it constantly:

  • AsRef<T> — an explicit, cheap “view me as a T“. Called by name.
  • Borrow<T> — the same, plus a promise that Eq/Ord/Hash agree. Used by HashMap::get.
  • Deref — an implicit conversion that also rewrites method resolution.

Only the third one changes what x.foo() means.

Remember the grade is compile + tests + clippy -D warnings.