RustTrait Bounds

Trait Bounds in Rust

Trait bounds constrain generic type parameters so the compiler knows which operations are available on them. Without a bound, you can do almost nothing with a generic T — you cannot call methods, print it, or compare it. Trait bounds are how you tell the compiler: "I only want to accept types that can do X."

Basic Syntax — Angle Bracket Form

The most explicit syntax places the bound directly after the type parameter, separated by a colon:

RUST
use std::fmt::Display;

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

fn main() {
    print_value(42);           // i32 implements Display
    print_value("hello");      // &str implements Display
    print_value(3.14);         // f64 implements Display
    // print_value(vec![1,2]); // Vec<i32> does NOT implement Display — compile error
}
Tip
The bound `T: Display` reads: "T must implement the Display trait." The compiler verifies this at every call site and rejects types that don't satisfy the bound.
impl Trait Syntax — The Shorthand

Rust 2018 introduced impl Trait as a shorter alternative for simple cases. Instead of naming the type parameter, you write impl TraitName directly in the parameter position:

RUST
use std::fmt::Display;

// Angle bracket form
fn print_a<T: Display>(value: T) {
    println!("{}", value);
}

// impl Trait form — equivalent for a single, independent parameter
fn print_b(value: impl Display) {
    println!("{}", value);
}

fn main() {
    print_a(100);
    print_b(100);
}
Note
`impl Trait` in parameter position is pure syntactic sugar — the compiler desugars it to an anonymous generic. The difference matters when you need to refer to the type by name (see multiple bounds below).
Multiple Bounds

Combine bounds with +. A type must implement all listed traits to satisfy the bound:

RUST
use std::fmt::{Debug, Display};

// T must implement both Display and Clone
fn clone_and_print<T: Display + Clone>(value: T) {
    let copy = value.clone();
    println!("Original: {}, Clone: {}", value, copy);
}

// Three bounds
fn describe<T: Display + Debug + Clone>(value: T) {
    println!("Display: {}", value);
    println!("Debug:   {:?}", value);
    let _copy = value.clone();
}

fn main() {
    clone_and_print(String::from("hello"));
    describe(42i32);
}
Where Clauses for Readability

When bounds grow complex, the inline syntax becomes hard to read. Move all bounds to a where clause placed after the return type:

RUST
use std::fmt::{Debug, Display};

// Hard to read inline
fn compare_inline<T: Display + Clone, U: Debug + Clone>(t: &T, u: &U) -> String {
    format!("{} and {:?}", t, u)
}

// Much cleaner with where
fn compare<T, U>(t: &T, u: &U) -> String
where
    T: Display + Clone,
    U: Debug + Clone,
{
    format!("{} and {:?}", t, u)
}

fn main() {
    let result = compare(&42, &vec![1, 2, 3]);
    println!("{}", result);
}
Tip
Use where clauses whenever you have more than one type parameter or more than two bounds per parameter. Consistent use of where clauses makes function signatures far easier to scan.
Bounds on Struct Definitions

You can place bounds on struct definitions, but this is generally discouraged. Bounds on structs propagate everywhere — even to code that doesn't use the bounded methods:

RUST
use std::fmt::Display;

// Bound on the struct — T: Display required everywhere Wrapper<T> appears
struct WrapperStrict<T: Display> {
    value: T,
}

// Preferred: no bound on struct definition
struct Wrapper<T> {
    value: T,
}

// Bound only where it is actually needed
impl<T: Display> Wrapper<T> {
    fn show(&self) {
        println!("{}", self.value);
    }
}

// Additional impl block with a different bound
impl<T: Clone> Wrapper<T> {
    fn clone_value(&self) -> T {
        self.value.clone()
    }
}
Note
The Rust compiler itself warns against bounds on struct definitions in many cases. Prefer bounds on impl blocks so the struct remains usable for all types.
Blanket Implementations

A blanket implementation implements a trait for every type that satisfies a bound. The standard library uses this extensively. The most famous example: every type that implements Display automatically implements ToString:

RUST
// This is (approximately) how the standard library does it:
// impl<T: Display> ToString for T {
//     fn to_string(&self) -> String {
//         format!("{}", self)
//     }
// }

// Because of this blanket impl, every Display type gets to_string() for free:
fn main() {
    let s = 42.to_string();        // i32: Display => i32: ToString
    let s2 = 3.14.to_string();     // f64: Display => f64: ToString
    let s3 = true.to_string();     // bool: Display => bool: ToString
    println!("{} {} {}", s, s2, s3);
}

You can write your own blanket implementations too:

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

// Blanket impl: every type that implements Display gets Describable for free
impl<T: std::fmt::Display> Describable for T {
    fn describe(&self) -> String {
        format!("Value: {}", self)
    }
}

fn main() {
    println!("{}", 99.describe());
    println!("{}", "rust".describe());
}
The Sized Bound and ?Sized

Every generic type parameter implicitly has a Sized bound, meaning the compiler must know its size at compile time. This is almost always what you want, but for types like str and [T] (which have no fixed size), you need to opt out with ?Sized:

RUST
// Implicit: fn generic<T: Sized>(x: T) — T must have a known size
fn generic<T>(x: T) { let _ = x; }

// Opt out of the Sized requirement — T can be a dynamically sized type
// Note: x must now be behind a reference because we can't put DSTs on the stack
fn flexible<T: ?Sized>(x: &T) {
    let _ = x;
}

fn main() {
    flexible(&42);          // i32 — Sized, works fine
    flexible("hello");      // str — NOT Sized, also works with ?Sized
    flexible(&[1, 2, 3][..]); // [i32] — also DST, works
}
Note
You will encounter `?Sized` most often in low-level or library code — for example, the standard library's `Box<T>` and `Rc<T>` use `T: ?Sized` so they can hold trait objects like `Box<dyn Trait>`.
Conditional Method Implementation

Trait bounds on impl blocks let you add methods to a generic type only when its type parameter meets certain criteria. Types that don't meet the bound still exist — they just don't have those methods:

RUST
use std::fmt::{Debug, Display};

struct Container<T> {
    items: Vec<T>,
}

// All Container<T> get this method
impl<T> Container<T> {
    fn new() -> Self {
        Container { items: Vec::new() }
    }
    fn push(&mut self, item: T) {
        self.items.push(item);
    }
    fn len(&self) -> usize {
        self.items.len()
    }
}

// Only Container<T> where T: Display gets this method
impl<T: Display> Container<T> {
    fn print_all(&self) {
        for item in &self.items {
            println!("{}", item);
        }
    }
}

// Only Container<T> where T: Debug gets this method
impl<T: Debug> Container<T> {
    fn debug_dump(&self) {
        println!("{:?}", self.items);
    }
}

fn main() {
    let mut c: Container<i32> = Container::new();
    c.push(1); c.push(2); c.push(3);
    c.print_all();   // available because i32: Display
    c.debug_dump();  // available because i32: Debug
}
Higher-Ranked Trait Bounds (HRTB)

Higher-ranked trait bounds let you express that a function must work for all lifetimes, not just a specific one. They appear with the for<'a> syntax and are most common when working with closures that take references:

RUST
// This bound says: F must implement Fn(&'a str) -> &'a str
// for ALL lifetimes 'a, not just one specific lifetime.
fn apply_to_str<F>(f: F, s: &str) -> &str
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    f(s)
}

fn main() {
    let result = apply_to_str(|s| s, "hello");
    println!("{}", result);
}
Note
In practice, the Rust compiler infers HRTB automatically in most closure situations. You only need to write `for<'a>` explicitly in rare cases involving function pointers or complex lifetime relationships.
dyn Trait vs impl Trait — Static vs Dynamic Dispatch

There are two ways to use a trait without naming the concrete type:

Feature

impl Trait

dyn Trait

Dispatch

Static (monomorphized)

Dynamic (vtable at runtime)

Performance

Faster — no indirection

Small overhead per call

Code size

Larger — one copy per type

Smaller — one shared copy

Heterogeneous collections

Not possible

Possible via Vec<Box<dyn Trait>>

Return position

All branches must return same type

Can return different types

Sized requirement

T is Sized (stack-allocated OK)

Must be behind pointer (&dyn or Box<dyn>)

RUST
use std::fmt::Display;

// Static dispatch — monomorphized, fast
fn print_static(value: impl Display) {
    println!("{}", value);
}

// Dynamic dispatch — trait object, one function for all types
fn print_dynamic(value: &dyn Display) {
    println!("{}", value);
}

// Only dyn allows heterogeneous collections
fn mixed_bag() {
    let items: Vec<Box<dyn Display>> = vec![
        Box::new(42),
        Box::new("hello"),
        Box::new(3.14),
    ];
    for item in &items {
        println!("{}", item);
    }
}

fn main() {
    print_static(1);
    print_dynamic(&2);
    mixed_bag();
}
Tip
Default to `impl Trait` (static dispatch). Switch to `dyn Trait` only when you need to store different concrete types together, or when the concrete type is not known until runtime.
Common Standard Library Bounds

Certain trait bounds appear constantly in Rust APIs. Recognizing them makes library documentation easier to read:

  • Send — the value can be transferred to another thread (required by most threading APIs)

  • Sync — a shared reference &T can be used from multiple threads (required for Arc<T>)

  • 'static — the type contains no non-static references; it can live for the entire program (required by thread::spawn)

  • Clone — the value can be explicitly duplicated

  • Default — the type has a sensible zero/empty default value

  • Sized — implicit on all generics; the type has a compile-time-known size

RUST
use std::thread;

// thread::spawn requires F: Send + 'static
// (the closure must be sendable to a new thread and own all its data)
fn run_in_thread<F: FnOnce() + Send + 'static>(f: F) {
    thread::spawn(f).join().unwrap();
}

fn main() {
    run_in_thread(|| println!("Hello from another thread!"));
}
Summary

Concept

Syntax / Pattern

When to Use

Basic bound

fn f<T: Trait>(x: T)

Single bound, need to name T

impl Trait param

fn f(x: impl Trait)

Simple single-use bound

Multiple bounds

T: A + B + C

Type must implement all listed traits

Where clause

where T: A + B, U: C

Multiple params or long bounds

Struct bound

struct S<T: Trait>

Avoid — prefer bounds on impl blocks

Blanket impl

impl<T: A> B for T

Give all A-types a B implementation

?Sized

T: ?Sized

Accept dynamically sized types

Conditional method

impl<T: Trait> S<T>

Methods for subset of instantiations

HRTB

for<'a> F: Fn(&'a T)

Closure valid for all lifetimes

Static dispatch

impl Trait

Default — fastest, no runtime overhead

Dynamic dispatch

dyn Trait

Heterogeneous collections or late binding