Skip to content
intermediate Phase 6 · Structs, Traits, and Trait Bounds

Generic Trait Bounds

Constrain type parameters with impl Trait syntax and where clauses.

45m
0 problems
Topic Progress 0%

Bounding a Type Parameter: Inline Bounds, `where` Clauses, and `impl Trait`

Write a generic function that formats its argument and watch it fail:

fn show<T>(value: T) {
    println!("{value}");        // error: `T` doesn't implement `std::fmt::Display`
}

T could be any type, so the compiler knows nothing about it: no methods, no traits, not even formatting. You already know how to write those behaviors — Traits and Implementations covered declaring and implementing them — so this topic is about demanding them in a signature. Stop claiming you accept everything and state what you need:

use std::fmt::Display;

fn show<T: Display>(value: T) {
    println!("{value}");        // legal: the bound guarantees Display
}

fn main() {
    show(1_i32);                // instantiates show::<i32>
    show("hi");                 // and show::<&str>
    // show(Point { x: 0, y: 0 });   // rejected: Point has no Display impl
}

A bound like T: Display is a contract in both directions. The declaration promises callers that any type they pass must implement Display; in exchange, the body may use every method Display provides. The compiler checks your side of the bargain at each call site, so a mistake surfaces where the type is chosen — not somewhere deep inside the body.

Several bounds combine with +, meaning and: T: Display + Clone + Send accepts only types implementing all three. When a signature outgrows one line, a where clause moves the constraints below it — same semantics, readable layout, and the form used on structs and impl blocks:

fn render<T, U>(t: &T, u: &U) -> String
where
    T: Display,
    U: Debug + Clone,
{
    format!("{t} {u:?}")          // both bounds usable inside
}

struct Cache<T> where T: Clone { entries: Vec<T> }   // bound lives with the type

impl<T: Clone> Cache<T> { fn duplicate(&self) -> Vec<T> { self.entries.clone() } }

(The last two forms were previewed in impl Blocks and Methods — an impl header bound makes its methods exist only for conforming types.)

Rust gives you a second spelling for the common case, impl Trait in argument position:

fn show(value: impl Display) { println!("{value}"); }

This is pure sugar for an anonymous generic parameter with the same bound — it monomorphizes identically and costs nothing at runtime. The difference is that the type has no name, and you cannot supply one at the call site:

error: cannot provide explicit generic arguments when `impl Trait` is used
       in argument position

show::<u32>(42) compiles against the generic form and fails against the sugar. Two impl Trait parameters are also independent types; whenever the signature must relate them (fn zip<A>(a: A, b: A)), you need named generics. Use impl Trait when the parameter type is incidental, named generics when the caller might need to steer inference with ::<>.

Return position is where impl Trait earns its keep:

fn summaries(items: &[Article]) -> impl Iterator<Item = String> + '_ {
    items.iter().map(|a| a.summarize())
}

The caller sees "an iterator of String" and nothing about how it is built — one concrete type is hidden behind the signature, and returning a different type in another branch is a compile error. That opacity is the opposite trade from dyn, where the implementation is picked at runtime; the comparison gets its own topic in Trait Objects and Dynamic Dispatch.

Form Example Choose it when
Inline bound fn f<T: Display>(t: T) one or two bounds, short signature
where clause fn f<T>(t: T) where T: Display + Clone many bounds, structs, impl blocks
Arg-position impl Trait fn f(t: impl Display) parameter type is incidental
Return-position impl Trait fn f() -> impl Iterator hide one concrete return type
Trait object fn f(t: &dyn Display) pick the implementation at run time

The interview staple hiding here: "what is the difference between fn f(x: impl Display) and fn f<T: Display>(x: T)?" — after type checking, none: same bound, same monomorphization, same machine code. The distinction is naming (you can write f::<u32>(..) only for the second) and where the syntax is allowed.

What the Body May Assume and What Monomorphization Costs

A bound decides what is legal inside the function. Look at the rule from both sides:

trait Summary { fn summarize(&self) -> String; }

fn report<T: Summary>(item: &T) {
    println!("{}", item.summarize());   // allowed — the bound promises it
    item.preview(40);                   // rejected — never promised anything
}

The second call fails even if every concrete type in your program happens to have a preview method: the signature only guarantees summarize, and the body is compiled against the signature, not against your call sites. The diagnostic names the fix:

error[E0599]: no method named `preview` found for type parameter `T` in the current scope
  = note: items from traits can only be used if the trait is implemented and in scope
  = help: consider restricting type parameter `T` with trait `Summary`

Read bounds as the smallest promise that makes the body compile — nothing more. Requiring Clone because you might need it later charges every caller for your uncertainty.

The other direction is where the error actually lands: a concrete type violating the contract is reported at the call site, with two spans — the declaration of the rule and the line that breaks it:

struct Point { x: i32, y: i32 }

fn show<T: Display>(value: T) { println!("{value}"); }

fn main() {
    show(Point { x: 0, y: 0 });
}
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:9:10
   |
 9 |     show(Point { x: 0, y: 0 });
   |          ^^^^^^^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   = note: required by a bound in `show`

The compiler is showing you the contract (required by a bound in show) and the breach (the argument) in one message. When you debug a missing bound, that is the reading order: find required by a bound in, then decide whether to implement the trait, convert the value (&x, x.to_string(), format!), or drop the bound from your signature if your body never needed it.

Now the payoff. Generic code is monomorphized: for every distinct type you instantiate, the compiler emits a separate copy of the function specialized to it.

show(1_i32)   →  a copy of show compiled for i32        ┐ direct call,
show("hi")    →  another copy for &str                  ┘ fully inlinable

There is no runtime type check, no vtable, no indirection: the call target is a fixed function the compiler can inline into the caller — which is why bounds are described as zero-cost. The bill arrives in binary size and compile time instead: N types over M generic functions means up to N × M copies, and a change to the function body recompiles all of them. For hot loops that trade is overwhelmingly worth it; for large codebases it is a reason generic-heavy APIs get a non-generic (often dyn) entry point.

Bounded generic dyn Trait (next topic)
Dispatch static, fixed at compile time dynamic, looked up in a vtable
Cost per call none — often inlined one indirect branch
Code size one copy per type one copy total
Type set frozen at compile time mixed at run time

One API-design consequence interviewers like: adding a bound to a public function is a breaking change. Every existing call site whose type lacks the new trait stops compiling, so semver-wise it lands like a signature change — tighten bounds deliberately, and prefer widening (T: Display + ?Sized) over narrowing when you must touch them.

Bounds are how the compiler proves behavior before the program runs. When the proof is impossible — because the set of implementations is only known at run time — you need the other mechanism: Trait Objects and Dynamic Dispatch.

Missing-Bound Autopsies: Reading E0277 and Escaping the Implicit `Sized` Trap

Most "missing trait" errors come from standard-library signatures you did not write. Learn the hidden requirements and half of E0277 disappears before it appears:

You write The bound it silently demands If the type lacks it
println!("{x}") Display E0277
assert_eq!(a, b) Debug + PartialEq E0277
v.sort() Ord E0277
collect::<HashMap<_, _>>() Hash + Eq E0277
thread::spawn(move || …) Send + 'static E0277

Every one of these is an ordinary trait bound in a std signature — the error message is simply the compiler reading that signature back to you. The resolution ladder is short: derive or implement the trait (#[derive(Debug, PartialEq, Hash, Eq)] covers most cases), convert the value at the boundary (&x, x.to_string(), format!("{x}")), or relax your own bound if your body was not actually using it. Note the recurring shape of the message: headline naming the pair, a = help: line naming the trait, and a = note: required by a bound in … pointing at the declaration — the last line is what tells you whether the requirement came from your code or from std.

The trap that catches people next is the bound nobody wrote. Every type parameter implicitly carries Sized:

struct Drawer<T> { item: Box<T> }      // really: struct Drawer<T: Sized>

let d = Drawer { item: Box::new(Circle { r: 1.0 }) };   // Drawer<Circle>: fine
let d2: Drawer<dyn Summary> = Drawer { item: my_box };  // fails
error[E0277]: the size for values of type `dyn Summary` cannot be known at
compilation time

A trait object has no fixed layout — its size depends on the concrete type hidden behind it — so it is !Sized and cannot satisfy the implicit bound, even though Box<dyn Summary> itself is a perfectly sized pointer. The escape is written with a question mark:

struct Drawer<T: ?Sized> { item: Box<T> }   // `?` reads as "maybe Sized"

let d2: Drawer<dyn Summary> = Drawer { item: my_box };

?Sized opts out of the implicit requirement; the field must then be an indirection (Box<T>, &T, Rc<T>), because an unsized value cannot sit inline in a struct. This is exactly how the standard library stores trait objects — Box<dyn Error>, Rc<dyn Fn()> — so T: ?Sized shows up in nearly every generic type that can hold one.

Two more failure modes deserve names. Over-bounding is not a compile error but an API defect: each bound you add is a constraint every caller must satisfy, and adding one later breaks code that used to compile — bound by what the body uses, nothing else. A concrete version of the same trap hides in #[derive]: a derived Debug on struct Wrapper<T> { inner: T } generates impl<T: Debug> Debug for Wrapper<T>, bounding every type parameter even when the field does not use it. Wrapper<Circle> therefore has no Debug until Circle does, and sometimes the right repair is dropping the derive for a five-line manual impl that leaves T unconstrained. Watch generated bounds the same way you watch handwritten ones.

And bounds are not limited to traits: lifetimes join the same list (where T: Trait + 'a, T: 'a alone), which is the subject of Lifetime Bounds and Outlives Relations.

Bounds resolve the type set at compile time. When the set cannot be known until the program is running — a plugin list, a heterogeneous collection, an error boxed behind one type — compilation-time selection is impossible, and you reach for the mechanism built for it.

Quiz

1. Inside `fn report<T: Summary>(item: &T)` you call `item.preview(40)`, even though every concrete type in the program has a `preview` method. What does the compiler do?

Question 1 options

2. Which statement comparing `fn show(value: impl Display)` and `fn show<T: Display>(value: T)` is accurate?

Question 2 options

3. An E0277 reports "`Point` doesn't implement `std::fmt::Display`" and includes "= note: required by a bound in `show`". What does the note identify?

Question 3 options

4. `struct Drawer<T> { item: Box<T> }` rejects `let d: Drawer<dyn Summary> = ...` with E0277 'the size for values of type `dyn Summary` cannot be known at compilation time'. What is the correct fix?

Question 4 options

Flashcards

Question

What does `T: Display` promise, and what does it permit?

Answer

It promises callers that only types implementing `Display` will be accepted — checked at each call site (E0277 otherwise) — and it permits the body to use every `Display` method. Anything not guaranteed by the bound is invisible inside the body, regardless of what concrete types actually arrive (E0599).

Question

Compare `fn f(x: impl Display)` with `fn f<T: Display>(x: T)`.

Answer

Identical after type checking: same bound, same monomorphization, same machine code. The difference is naming — the generic form accepts a turbofish (`f::<u32>(..)`) and can relate parameters (`fn f<A>(a: A, b: A)`), while `impl Trait` is an anonymous parameter that rejects explicit generic arguments at the call site.

Question

What are the two locations in a typical E0277, and what does each tell you?

Answer

The failing expression (whose type lacks the trait — the breach) and `= note: required by a bound in ...` (the declaration demanding it — the contract). Reading the note decides the fix: derive/implement the trait, convert the value, or drop a bound your body never needed.

Question

Why does `Drawer<dyn Summary>` fail while `Box<dyn Summary>` is fine?

Answer

Every type parameter implicitly carries `Sized`; a trait object has no compile-time size so it violates that bound (E0277), whereas `Box<dyn Summary>` is itself a sized pointer. Fix: `struct Drawer<T: ?Sized> { item: Box<T> }` — `?Sized` opts out of the implicit bound and the field stays a pointer.

Question

What does monomorphization buy, what does it cost, and what breaks when you tighten a bound?

Answer

It buys static dispatch with direct, inlinable calls and zero runtime checks. It costs one machine-code copy per (function, type) pair — binary size and compile time. And adding a bound to a public function is a breaking change: every call site whose type lacks the new trait stops compiling.

Revision Notes

Key Takeaways

  • 1. A bound is bidirectional: callers must satisfy `T: Trait`, and the body may use exactly those methods — no more, no matter what concrete types arrive
  • 2. Errors surface where the decision is made: E0277 at the call site (with `required by a bound in …`), E0599 inside the body when the bound was never declared
  • 3. `impl Trait` in argument position is sugar for an anonymous generic (same machine code, no turbofish); in return position it hides one concrete type
  • 4. Bounds monomorphize: no dispatch cost, calls inline, but code size grows per instantiation and tightening a public bound breaks callers
  • 5. `Sized` is implied for every type parameter; `T: ?Sized` is how generic code holds `Box<dyn Trait>` — an unsized value always lives behind a pointer

Interview Tips

  • • "\"What does `T: Display` actually buy you?\" — split the answer: it constrains what callers may pass, and it licenses exactly `Display`'s methods in the body; add that the check happens at the call site, which is why E0277 points there"
  • • "\"Generic vs trait object — which is faster and why?\" — generics are monomorphized into a direct, inlinable call with zero overhead; `dyn` costs an indirect branch and blocks inlining. Mention the counterweight: code size and compile time"
  • • "\"Is adding a trait bound to a public function a breaking change?\" — yes: every call site with a non-conforming type stops compiling; contrast with *removing* a bound, which only widens what compiles"
  • • "\"Why won't `Drawer<dyn Summary>` compile when `Box<dyn Summary>` is a normal-sized pointer?\" — the implicit `Sized` bound on `T`; fix with `T: ?Sized` and an indirection field; naming E0277's 'size … cannot be known at compilation time' shows you have debugged it"

Cheat Sheet

Generic Trait Bounds

Core form:

fn show<T: Display>(value: T) { println!("{value}"); }   // T: promise + power

fn render<T, U>(t: &T, u: &U) -> String      // where clause: same rules, readable
where
    T: Display,
    U: Debug + Clone,

fn show2(value: impl Display) { … }          // sugar for an anonymous generic
fn it(items: &[A]) -> impl Iterator<Item = String> + '_ { … }   // one hidden type
Form Reached for when
T: A + B inline short signature, 1–2 bounds
where clause long signatures, struct/impl headers
impl Trait (arg) parameter type incidental — no turbofish
impl Trait (ret) hide exactly one concrete type
&dyn Trait choose the implementation at run time

Semantics:

  • Body may only use what the declared bounds guarantee → violating call: E0599 + consider restricting type parameter T with trait X
  • Call site violating a bound → E0277: headline + = note: required by a bound in … (contract and breach in one message)
  • Instantiation is monomorphized: direct call, inlinable, zero runtime cost — N types × M generics ≈ N×M copies
  • Adding a bound to a public fn is a breaking change; ?Sized removes the implicit Sized (required for Box<dyn Trait> fields)

Hidden std bounds: println!→Display · assert_eq!→Debug+PartialEq · sort→Ord · HashMap→Hash+Eq · spawn→Send+'static

Continue Learning