Type System Fundamentals

Trait Bounds

Trait bounds (T: Trait) constrain generic type parameters in Rust's type system, specifying the exact capabilities and lifetime requirements a type must satisfy so the compiler can type-check a generic body once—before monomorphization ever occurs.

Definition-Site Type Checking (Why Rust Generics Are Not C++ Templates)

In C++ templates, an unconstrained template template<typename T> T add(T a, T b) { return a + b; } is only type-checked when instantiated with a concrete type, producing deep instantiation tracebacks when a type lacks +.

In Rust, generic functions are type-checked at their definition site against their declared trait bounds:

  • Inside fn process<T>(x: T), you cannot call any method, operator, or clone on x unless that capability is declared in T's trait bounds (T: Clone, T: Add, T: Display).
  • Conversely, once fn process<T: Bound>(x: T) compiles, the compiler guarantees that every caller type T satisfying Bound will compile inside process without internal type errors.

Four Forms of Trait and Lifetime Bounds

Bound Form Syntax Meaning
Single / Multiple trait bounds T: Clone + Debug T must implement both Clone and Debug
Argument-position impl Trait fn f(x: impl Display + Clone) Shorthand for an anonymous generic type parameter
Lifetime outlives bound T: 'a or T: 'static Any references contained inside T must live at least as long as 'a (or 'static)
Higher-Ranked Trait Bound (HRTB) F: for<'a> Fn(&'a str) -> bool F satisfies the bound for every possible lifetime 'a
use std::fmt::{Debug, Display};

fn format_and_clone<T>(item: &T) -> (String, T)
where
    T: Display + Debug + Clone,
{
    let formatted = format!("display={item}, debug={item:?}");
    (formatted, item.clone())
}

fn main() {
    let (summary, copy) = format_and_clone(&42_u32);
    println!("{summary} | cloned={copy}");
}
display=42, debug=42 | cloned=42

Conditional Method Availability via Bounded impl Blocks

Placing different trait bounds on separate impl<T: ...> Wrapper<T> blocks enables type-conditional APIs where methods exist on Wrapper<T> if and only if T satisfies the corresponding bound:

#[derive(Debug)]
struct BoxedValue<T>(T);

impl<T: Copy> BoxedValue<T> {
    fn duplicate(&self) -> (T, T) {
        (self.0, self.0)
    }
}

impl<T: std::ops::Add<Output = T> + Copy> BoxedValue<T> {
    fn doubled(&self) -> T {
        self.0 + self.0
    }
}

fn main() {
    let num = BoxedValue(21);
    let label = BoxedValue("ok"); // `&str` is `Copy`, but does NOT implement `Add`

    println!("num duplicate: {:?}, doubled: {}", num.duplicate(), num.doubled());
    println!("label duplicate: {:?}", label.duplicate());
    // `label.doubled()` does not exist because `&str: !Add`
}
num duplicate: (21, 21), doubled: 42
label duplicate: ("ok", "ok")