Skip to content

← Generics and Traits step 19 of 24

Hard Primitives

Blanket impls, and why you can never carve out an exception

You have already used three blanket impls without writing one:

impl<T: Display + ?Sized> ToString for T { ... }      // Display => .to_string()
impl<T, U: From<T>> Into<U> for T { ... }             // From    => .into()
impl<T, U: TryFrom<T>> TryInto<U> for T { ... }       // TryFrom => .try_into()

Each is a single impl covering an unbounded number of types, conditional on a bound. This item is where you write one and then discover its permanent consequence.

Your task

pub trait Describe { fn describe(&self) -> String; }

impl<T: Debug> Describe for T { ... }

pub fn run(nums: Vec<i64>, label: String, flag: bool) -> Vec<String>

describe renders "{type_name}: {value:?}" — the fully-qualified type name from std::any::type_name::<T>(), a colon and a space, then the Debug rendering. run calls it on five values and returns the results in order:

alloc::vec::Vec<i64>: [1, 2, 3]
alloc::string::String: "hi"
bool: true
u8: 42
&str: "hi"

Nothing in run changes; the whole exercise is the impl block. Notice that the last three call describe on a bool literal, a u8 literal and a string literal — types you did not write and could not modify. That is what a blanket impl of your own trait buys you.

Two details in that output worth reading twice. The String line shows quotes because Debug for strings escapes and quotes; the &str line shows the type as &str, not str, because the blanket impl carries an implicit T: Sized bound and str is unsized — so method resolution autorefs one step further and lands on &str.

The consequence

Once your impl works, add this:

impl Describe for String {
    fn describe(&self) -> String { format!("a string: {self}") }
}
error[E0119]: conflicting implementations of trait `Describe`
              for type `String`
   = note: upstream crates may add a new impl of trait `Debug` ...

The starter ships that String impl, so this is the first thing you see. Rust has no stable specialisation, so “blanket impl plus one override” is simply not expressible. There is no default fn, no priority ordering, no escape. If you publish a trait with a blanket impl, nobody — including you — can ever write a more specific impl for it.

That makes a blanket impl over an unbounded T a significant API commitment, and one of the standard mistakes in a first Rust library. Two rules of thumb:

  • Bound it as tightly as you can. impl<T: Debug> Describe for T at least excludes non-Debug types. impl<T> Describe for T would exclude nothing and make the trait useless to everyone.
  • Blanket-impl your own trait, never a foreign one. impl<T: Debug> Describe for T is fine here because Describe is local. impl<T: Debug> Display for T would be E0117 — the orphan rule — and even impl<T> MyTrait for Vec<T> can hit E0210 depending on where the parameters sit. The next problem takes those two codes apart properly.

Why the compiler is so cautious

Read that error note again: “upstream crates may add a new impl of trait Debug. Even if no type in your program is both a String and something weird, the compiler reasons about what a future version of another crate could do. Coherence is a whole-program property that has to survive recompilation against new dependency versions, so the rules are deliberately conservative.

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