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:
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
}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:
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);
}Multiple Bounds
Combine bounds with +. A type must implement all listed traits to satisfy the bound:
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:
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);
}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:
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()
}
}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:
// 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:
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:
// 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
}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:
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:
// 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);
}dyn Trait vs impl Trait — Static vs Dynamic Dispatch
There are two ways to use a trait without naming the concrete type:
Feature |
|
|
|---|---|---|
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 |
Return position | All branches must return same type | Can return different types |
Sized requirement | T is Sized (stack-allocated OK) | Must be behind pointer ( |
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();
}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&Tcan be used from multiple threads (required forArc<T>)'static— the type contains no non-static references; it can live for the entire program (required bythread::spawn)Clone— the value can be explicitly duplicatedDefault— the type has a sensible zero/empty default valueSized— implicit on all generics; the type has a compile-time-known size
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 |
| Single bound, need to name T |
impl Trait param |
| Simple single-use bound |
Multiple bounds |
| Type must implement all listed traits |
Where clause |
| Multiple params or long bounds |
Struct bound |
| Avoid — prefer bounds on impl blocks |
Blanket impl |
| Give all A-types a B implementation |
?Sized |
| Accept dynamically sized types |
Conditional method |
| Methods for subset of instantiations |
HRTB |
| Closure valid for all lifetimes |
Static dispatch |
| Default — fastest, no runtime overhead |
Dynamic dispatch |
| Heterogeneous collections or late binding |