Generic Constraints
A generic constraint (also called a trait bound) is a compile-time requirement attached to a generic type parameter (T: Trait) specifying which traits a concrete type must implement to be substituted for T.
Why Unconstrained Generics Are Restricted
Without constraints, the compiler assumes nothing about T beyond the default implicit Sized bound. A function with an unconstrained <T> cannot add, compare, clone, or print values of type T because not every Rust type supports those operations.
Unconstrained <T> Constrained <T: Clone + PartialOrd + Display>
+-------------------------+ +---------------------------------------------+
| Allowed: | | Allowed: |
| - move value | | - everything on unconstrained T |
| - borrow (&T, &mut T) | | - value.clone() |
| - drop value | | - a < b, a <= b, a > b, a >= b |
| - std::mem::size_of<T> | | - format!("{value}") |
+-------------------------+ +---------------------------------------------+
Inline Bound Syntax vs. where Clauses
Rust provides two equivalent ways to write generic constraints:
- Inline bounds inside the
<...>parameter list (<T: Clone + Debug>), suited for short signatures with one or two bounds. whereclauses placed just before the opening{of the function orimplblock, suited for multiple parameters, associated types, or complex multi-trait constraints.
use std::fmt::{Debug, Display};
use std::ops::Add;
// 1. Inline constraint syntax:
fn double_value<T: Copy + Add<Output = T>>(x: T) -> T {
x + x
}
// 2. Equivalent `where` clause syntax on a multi-parameter signature:
fn log_addition<A, B, Out>(left: A, right: B) -> Out
where
A: Copy + Display + Add<B, Output = Out>,
B: Copy + Display,
Out: Debug,
{
let result = left + right;
println!("{left} + {right} = {result:?}");
result
}
fn main() {
println!("double_value(21) = {}", double_value(21));
let sum = log_addition(12.5_f64, 7.5_f64);
println!("returned: {sum}");
}
double_value(21) = 42
12.5 + 7.5 = 20.0
returned: 20
Combining Multiple Constraints with +
When a type parameter must satisfy more than one trait simultaneously, join the traits with +. A concrete type argument is accepted only if it implements every trait in the + list:
#[derive(Debug, Clone)]
struct Config {
threads: usize,
}
fn duplicate_and_inspect<T: Clone + std::fmt::Debug>(item: &T) -> (T, T) {
let first = item.clone();
let second = item.clone();
println!("Duplicated: {first:?}");
(first, second)
}
fn main() {
let cfg = Config { threads: 8 };
let (a, b) = duplicate_and_inspect(&cfg);
println!("a.threads = {}, b.threads = {}", a.threads, b.threads);
}
Duplicated: Config { threads: 8 }
a.threads = 8, b.threads = 8
Relaxing the Implicit Sized Constraint with ?Sized
Every generic type parameter T in Rust carries an implicit T: Sized constraint by default, meaning T's byte size must be known at compile time. Dynamically sized types (DSTs) such as str and [u8] do not implement Sized.
To allow a generic function or struct to accept dynamically sized types behind a reference or pointer, write T: ?Sized ("T may or may not be Sized"):
use std::fmt::Debug;
fn inspect_ref<T: Debug + ?Sized>(value: &T) {
println!("Inspected: {value:?}");
}
fn main() {
// Works with Sized types (&i32, &Vec<i32>):
inspect_ref(&42);
// Also works with unsized slices (&str, &[i32]) because of `?Sized`:
inspect_ref("unsized string slice");
inspect_ref(&[1, 2, 3][..]);
}
Inspected: 42
Inspected: "unsized string slice"
Inspected: [1, 2, 3]
| Bound Syntax | Meaning |
|---|---|
<T> |
T must be Sized (implicit default) |
<T: Display> |
T must implement Display and be Sized |
<T: Display + Clone> |
T must implement both Display and Clone (and Sized) |
<T: ?Sized> |
T may be either Sized or dynamically sized (str, [U], dyn Trait) |
<T: 'static> |
T cannot contain any non-'static borrowed references |