where Clauses
A where clause is a block of trait and lifetime constraints placed after a function signature, struct/enum header, trait definition, or impl header—immediately before the opening { or terminating ;.
Separating Type Signatures from Bounds
When generic parameters carry several bounds, inline syntax clutters the parameter list and pushes the return type far to the right. Moving constraints into a where clause keeps the function name, parameters, and return type compact:
use std::fmt::{Debug, Display};
use std::hash::Hash;
// Inline bounds (harder to scan):
fn register_inline<K: Clone + Eq + Hash + Display, V: Clone + Debug + Default>(
key: K,
value: Option<V>,
) -> (K, V) {
(key, value.unwrap_or_default())
}
// Equivalent `where` clause (clean signature):
fn register<K, V>(key: K, value: Option<V>) -> (K, V)
where
K: Clone + Eq + Hash + Display,
V: Clone + Debug + Default,
{
(key, value.unwrap_or_default())
}
fn main() {
let pair = register_inline("workers", Some(4_usize));
let fallback = register("workers", None::<usize>);
println!("pair: {pair:?}, fallback: {fallback:?}");
}
pair: ("workers", 4), fallback: ("workers", 0)
Capabilities Exclusive to where Clauses
Unlike inline bounds (<T: Trait>), which can only place a simple type or lifetime parameter on the left side of :, a where clause allows any arbitrary type expression on the left side of ::
| Constraint Kind | where Clause Syntax |
Expressible Inline? |
|---|---|---|
| Simple type parameter | where T: Clone |
Yes (<T: Clone>) |
| Associated type bound | where I::Item: Display |
No (except equality <I: Iterator<Item = u8>>) |
| Composite / reference type | where &'a T: IntoIterator |
No |
| Tuple or concrete wrapper | where (T, U): Debug, where Option<T>: Clone |
No |
| Higher-ranked trait bound (HRTB) | where F: for<'a> Fn(&'a str) -> bool |
Yes, but usually written in where |
1. Constraining Associated Types
When a function accepts an iterator or graph type and needs a trait bound on its associated type (I::Item: PartialOrd + Copy), a where clause expresses that directly without introducing an extra dummy type parameter:
fn min_and_max<I>(iter: I) -> Option<(I::Item, I::Item)>
where
I: IntoIterator,
I::Item: Copy + Ord,
{
let mut it = iter.into_iter();
let first = it.next()?;
let mut min = first;
let mut max = first;
for val in it {
if val < min {
min = val;
}
if val > max {
max = val;
}
}
Some((min, max))
}
fn main() {
println!("{:?}", min_and_max([42, 7, 19, 88, 3]));
}
Some((3, 88))
2. Constraining Reference Types (where for<'a> &'a T: IntoIterator)
Many collection types (Vec<u8>, [i32; 4], BTreeSet<String>) implement IntoIterator on a shared reference &'a C so they can be iterated over multiple times without being consumed. A where clause can require that &'a C is iterable:
use std::fmt::Display;
fn print_twice<'a, C>(collection: &'a C)
where
&'a C: IntoIterator,
<&'a C as IntoIterator>::Item: Display,
{
for pass in 1..=2 {
print!("Pass {pass}:");
for item in collection {
print!(" {item}");
}
println!();
}
}
fn main() {
let items = vec!["rust", "cargo", "clippy"];
print_twice(&items);
}
Pass 1: rust cargo clippy
Pass 2: rust cargo clippy
3. Method-Level Eligibility Gates (where Self: ...)
Inside a trait definition, adding where Self: Sized (or another trait bound) to an individual method restricts that specific method to implementing types that satisfy the bound, while keeping the rest of the trait usable on unsized types (dyn Trait):
trait Describable {
fn describe(&self) -> String;
// Only callable on concrete Sized types, so `Describable` remains dyn-compatible:
fn into_boxed_description(self) -> String
where
Self: Sized,
{
self.describe()
}
}
struct Tag(&'static str);
impl Describable for Tag {
fn describe(&self) -> String {
format!("Tag({})", self.0)
}
}
fn main() {
let dyn_ref: &dyn Describable = &Tag("v1");
println!("Via dyn ref: {}", dyn_ref.describe());
println!("Via owned: {}", Tag("v2").into_boxed_description());
}
Via dyn ref: Tag(v1)
Via owned: Tag(v2)