Skip to content
rusthacks.dev
Generics

Blanket Implementations in Rust and Their Coherence Limits

A blanket implementation implements a trait for every type that meets a bound. See how ToString and Into work, the coherence rules that limit you and a safe pattern.

6 min read
Summarize in AI
Flat illustration for the post “Blanket Implementations in Rust and Their Coherence Limits”, composed around architecture.

A blanket implementation implements a trait for every type that meets some condition, in one impl block: impl<T: Display> Describe for T. It is how the standard library gives you .to_string() on anything printable and .into() on anything with a From impl, and it is how you can give your own trait to every type that already has some capability, including types you did not write. It is also the feature that most often runs into the coherence rules, the E0119 and E0210 errors. This post writes one, runs it, explains the ?Sized detail that breaks it for str and the compiler error you get without it, and covers the limits that decide whether a blanket is viable. All of it compiled on Rust 1.99 in edition 2024.

A blanket implementation, working

src/main.rsrust
use std::fmt::Display;

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

// A blanket implementation: every type that is Display gets Describe for free.
// `?Sized` lets it apply to unsized types such as `str` too.
impl<T: Display + ?Sized> Describe for T {
    fn describe(&self) -> String {
        format!("<<{self}>>")
    }
}

// The standard library does the same thing: `impl<T: Display + ?Sized> ToString for T`.
struct Meters(f64);

impl Display for Meters {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} m", self.0)
    }
}

fn describe_all(items: &[&dyn Describe]) -> Vec<String> {
    items.iter().map(|i| i.describe()).collect()
}

fn main() {
    println!("{}", 42.describe());
    println!("{}", "plain str".describe());
    println!("{}", Meters(1.5).describe());
    println!("{}", Meters(1.5).to_string());
    println!("{:?}", describe_all(&[&1, &"two", &Meters(3.0)]));
    let boxed: Box<str> = "boxed".into();
    println!("{}", boxed.describe());
}
Text
<<42>>
<<plain str>>
<<1.5 m>>
1.5 m
["<<1>>", "<<two>>", "<<3 m>>"]
<<boxed>>

The trait Describe is implemented once, for T where T: Display + ?Sized. After that, every printable type has .describe(): the integer 42, a string slice, the custom Meters that implements Display, even Box<str>. Nobody wrote an impl Describe for i32 or an impl Describe for Meters. The blanket covers all of them, including types defined later in other crates, as long as they implement Display. The collection call in describe_all shows the other half: because every Display type implements Describe, a &dyn Describe can wrap an integer, a string and a Meters in one slice.

The standard library's version of this is impl<T: Display + ?Sized> ToString for T, and the fourth line of output, Meters(1.5).to_string(), is that blanket at work: Meters implemented Display, and ToString followed with no code. Into for every From, IntoIterator for every Iterator, and Borrow<T> for every T are the other blankets you use daily.

The ?Sized detail

Generic parameters carry an implicit Sized bound. Without writing anything, T in impl<T: Display> means a type whose size is known at compile time. That excludes str and [T] and dyn Trait, which are unsized, so a blanket written without ?Sized silently skips them. The line impl<T: Display + ?Sized> relaxes the implicit bound ("T may be unsized"), and the impl then covers str as well.

Most call sites do not notice, because method syntax auto-references: "plain str".describe() finds Describe for &str, which is sized and implements Display. The difference appears when a function insists on the unsized type itself. This function takes &D with D: Describe + ?Sized so it can accept a &str as &D with D = str:

src/main.rsrust
impl<T: Display> Describe for T {
    fn describe(&self) -> String {
        format!("<<{self}>>")
    }
}

fn show<D: Describe + ?Sized>(value: &D) -> String {
    value.describe()
}

let text: &str = "plain str";
println!("{}", show(text));
Text
error[E0277]: the trait bound `str: Describe` is not satisfied
  --> t173z.rs:20:25
   |
20 |     println!("{}", show(text));
   |                    ---- ^^^^ the trait `Sized` is not implemented for `str`
   |                    |
   |                    required by a bound introduced by this call
   |
note: required for `str` to implement `Describe`
  --> t173z.rs:7:18
   |
 7 | impl<T: Display> Describe for T {
   |      -           ^^^^^^^^     ^
   |      |
   |      unsatisfied trait bound implicitly introduced here

The note is the diagnosis: "unsatisfied trait bound implicitly introduced here" points at the T that carries a hidden Sized. The fix is to write ?Sized on the blanket, impl<T: Display + ?Sized> Describe for T, after which the same program prints <<plain str>>. Rule of thumb: put ?Sized on every blanket implementation whose body only needs &self, which is nearly all of them. The standard library does for ToString, AsRef, Borrow and others.

The coherence limits

A blanket implementation is the most powerful thing the trait system offers and the most restricted. Two rules bound it.

You may write a blanket for your own trait. impl<T: Display> Describe for T is accepted because Describe is local, which satisfies the orphan rule from the trait side. You may not write one for a foreign trait. Trying to implement Display for every T is rejected at the declaration, with an error about the uncovered type parameter:

Text
error[E0210]: type parameter `T` must be used as an argument to some local type (e.g., `MyStruct<T>`)
 --> t173y.rs:3:6
  |
3 | impl<T> fmt::Display for T {
  |      ^ uncovered type parameter
  |
  = note: implementing a foreign trait is only possible if at least one of the types for which it is implemented is local
  = note: only traits defined in the current crate can be implemented for a type parameter

That message is the orphan rule applied to a type parameter. If it were allowed, your crate would claim Display for every type, including those in crates that define their own, and the two would collide. See the orphan rule and E0117 for the general statement.

The second limit is overlap. Once Describe has a blanket over all Display types, no other implementation of Describe can exist for a type that is Display, because the two would conflict. You cannot add a more specific impl Describe for String later; the compiler reports E0119. Conflicting implementations shows two ways out, narrowing the blanket with a marker trait or replacing the second impl with an overridable default method. Think of a blanket as a promise that no one, including you, will special-case a type that qualifies.

Adding a blanket later is a breaking change

Because of that promise, adding a blanket implementation to a published trait can break users. If a downstream crate had impl Describe for MyType and MyType: Display, a new blanket makes their impl conflict. The library guidelines treat adding a blanket as major-version territory unless the trait is sealed. Plan blankets at the time you design the trait: decide whether the trait is "automatically implemented for anything with capability X", in which case provide the blanket from the start and let nobody implement it by hand, or "implemented explicitly by types that opt in".

Patterns that use blankets well

An extension trait with a blanket gives every type of some kind new methods, which is how Itertools adds methods to every Iterator: impl<I: Iterator> Itertools for I {} with provided methods, so the blanket body is empty. A bundle-of-bounds trait names a combination: trait Number: Copy + Add<Output = Self> {} with impl<T: Copy + Add<Output = T>> Number for T {} lets signatures say T: Number. A forwarding blanket makes references work like values: impl<T: Describe + ?Sized> Describe for &T lets &x be used where x could, and the standard library does this for most traits. A conversion blanket powers Into and TryFrom, which you get by implementing the reverse trait.

For the neighboring features, supertraits show how bounds compose without a blanket, and extension traits develop the second pattern in full.

If you remember one habit from this post, make it the ?Sized on a blanket. It costs six characters, it widens the impl to slices, strings and trait objects, and it avoids the confusing "implicitly introduced bound" error later, when someone writes a function generic over your trait and passes it a &str.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.