Trait Bounds on Structs and Enums
A struct or enum definition can place trait bounds directly on its generic type parameters (struct SortedVec<T: Ord>), or leave the type definition unconstrained (struct SortedVec<T>) and place trait bounds only on the impl<T: Ord> blocks that require them.
Why Type Definitions Usually Omit Trait Bounds
When you place a trait bound directly on a struct or enum definition, every single impl block, function signature, and type annotation that mentions that type must repeat (or satisfy) that bound—even for operations like len() or is_empty() that never use the trait:
// Bound placed on the struct definition itself:
struct Cache<K: Eq + std::hash::Hash, V> {
entries: Vec<(K, V)>,
}
// Even `len()`, which never hashes or compares `K`, is forced to repeat `K: Eq + Hash`:
impl<K: Eq + std::hash::Hash, V> Cache<K, V> {
fn len(&self) -> usize {
self.entries.len()
}
}
fn main() {
let cache: Cache<&str, i32> = Cache {
entries: vec![("hits", 10)],
};
println!("Cache len: {}", cache.len());
}
Cache len: 1
Omitting K: Eq + std::hash::Hash on the impl block when the struct definition requires it fails with error E0277:
// ❌ does not compile
struct Cache<K: Eq, V> {
entries: Vec<(K, V)>,
}
impl<K, V> Cache<K, V> {
fn len(&self) -> usize {
self.entries.len()
}
}
fn main() {}
error[E0277]: the trait bound `K: Eq` is not satisfied
--> src/main.rs:6:12
|
6 | impl<K, V> Cache<K, V> {
| ^^^^^^^^^^^ the trait `Eq` is not implemented for `K`
|
note: required by a bound in `Cache`
--> src/main.rs:2:17
|
2 | struct Cache<K: Eq, V> {
| ^^ required by this bound in `Cache`
help: consider restricting type parameter `K` with trait `Eq`
|
6 | impl<K: std::cmp::Eq, V> Cache<K, V> {
| ++++++++++++++
Idiomatic Pattern: Unconstrained Type Definition + Constrained impl Blocks
The standard library (HashMap<K, V>, BTreeSet<T>, Vec<T>) leaves struct and enum definitions unconstrained and attaches trait bounds only to the specific impl blocks that use those traits:
#[derive(Debug)]
struct MinTracker<T> {
current_min: Option<T>,
}
// Unconstrained impl block: works for ANY type T
impl<T> MinTracker<T> {
fn new() -> Self {
Self { current_min: None }
}
fn into_inner(self) -> Option<T> {
self.current_min
}
}
// Constrained impl block: only requires `T: Ord` when inserting/comparing
impl<T: Ord> MinTracker<T> {
fn observe(&mut self, candidate: T) {
match &self.current_min {
Some(min) if &candidate >= min => {}
_ => self.current_min = Some(candidate),
}
}
}
fn main() {
let mut tracker = MinTracker::new();
tracker.observe(40);
tracker.observe(12);
tracker.observe(25);
println!("Minimum observed: {:?}", tracker.into_inner());
}
Minimum observed: Some(12)
Two Legitimate Cases for Trait Bounds on Structs or Enums
There are two specific cases where a struct or enum definition must declare a trait bound on its type parameter:
1. Referencing an Associated Type Inside a Field or Variant
If a field or enum variant stores an associated type such as T::Owned (from ToOwned) or I::Item (from Iterator), the struct or enum definition must bound T so the compiler knows the associated type exists:
use std::fmt::Debug;
#[derive(Debug)]
struct PeekBuffer<I: Iterator> {
iter: I,
peeked: Option<I::Item>,
}
impl<I: Iterator> PeekBuffer<I> {
fn new(mut iter: I) -> Self {
let peeked = iter.next();
Self { iter, peeked }
}
}
fn main() {
let buf = PeekBuffer::new([10, 20, 30].into_iter());
println!("First peeked item: {:?}", buf.peeked);
}
First peeked item: Some(10)
2. Implementing Drop That Calls Trait Methods on T
Rust requires any impl<T: Bound> Drop for Struct<T> to have the exact same bounds as the struct definition itself (E0367). If your Drop implementation must call a trait method on T (such as flushing an inner writer), the struct definition itself must declare that bound:
trait Flushable {
fn flush(&mut self);
}
struct AutoFlush<W: Flushable> {
writer: W,
}
impl<W: Flushable> Drop for AutoFlush<W> {
fn drop(&mut self) {
self.writer.flush();
}
}
struct BufferLog(Vec<&'static str>);
impl Flushable for BufferLog {
fn flush(&mut self) {
println!("Flushing {} buffered log lines on drop", self.0.len());
self.0.clear();
}
}
fn main() {
let _guard = AutoFlush {
writer: BufferLog(vec!["boot", "ready"]),
};
}
Flushing 2 buffered log lines on drop