16.3Trait bounds
This is the payoff. Chapter 15 left largest unfinished: a generic function whose body compared two Ts with >, which the compiler rejected because not every type can be compared. Traits are the missing piece. A trait bound restricts a type parameter to types that implement a given trait, which tells the compiler exactly what T can do, and the moment we add the right bound, largest compiles.
The syntax: T: Trait
A trait bound is written with a colon after the type parameter: T: Trait means "T is some type that implements Trait." Here is largest, finally complete:
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", largest(3, 7));
println!("{}", largest('a', 'z'));
println!("{}", largest(2.5, 1.0));
}7
z
2.5
The only change from chapter 15's broken version is <T: PartialOrd> instead of <T>. PartialOrd is the standard-library trait for the comparison operators (<, >, <=, >=), so T: PartialOrd promises "T is a type you can compare with >." Now the body's a > b is justified: the bound guarantees every T this function is ever called with supports comparison, and the compiler checks at each call that the type actually implements PartialOrd. Call largest with i32, char, f64, all of which implement PartialOrd, and it works; call it with a type that doesn't, and that call is rejected, with a clear message, rather than the function definition being broken.
This is exactly what chapter 15's E0369 error and its help line predicted. The compiler told you to add T: PartialOrd; you added it; the function compiles. The cliffhanger is resolved.
Key insight
A trait bound is the bridge between generics and traits. Generics give you a type parameter T; a trait bound tells the compiler what T can do, which is what lets the body actually use it. Without bounds, a generic body can only move values around (chapter 15); with bounds, it can compare them, print them, add them, anything the bounding traits provide. "Generic over any type that can be compared" is a far more useful thing to say than "generic over any type," and trait bounds are how you say it.
Multiple bounds and where clauses
A type parameter can require several traits at once, joined with +. Suppose a function needs to both print and compare its values:
use std::fmt::Display;
fn largest_and_announce<T: PartialOrd + Display>(a: T, b: T) -> T {
let winner = if a > b { a } else { b };
println!("the winner is {winner}");
winner
}
T: PartialOrd + Display means "T implements both PartialOrd (so we can compare) and Display (so we can print with {})." The body uses both capabilities, so it needs both bounds. When bounds pile up and the signature gets crowded, you can move them into a where clause after the return type, which many find more readable:
fn process<T, U>(a: T, b: U) -> String
where
T: Display + PartialOrd,
U: Clone + Display,
{
format!("{a} and {b}")
}
The where clause says the same thing as inline bounds but keeps the fn process<T, U>(...) line uncluttered. Use inline bounds for one or two simple constraints, a where clause when there are several or they're long. They're interchangeable; it's purely a readability choice.
Bounds on generic types and methods
Bounds aren't just for functions. A generic impl block can require a bound, so a method exists only when the type parameter supports it, the more general version of lesson 15.4's impl Point<f64> trick:
struct Pair<T> {
first: T,
second: T,
}
impl<T: PartialOrd + Display> Pair<T> {
fn print_larger(&self) {
if self.first >= self.second {
println!("larger: {}", self.first);
} else {
println!("larger: {}", self.second);
}
}
}
Pair<T> exists for any T, but print_larger is defined only in an impl block bounded by PartialOrd + Display. So a Pair<i32> has print_larger (i32 is both comparable and printable), while a Pair of some type that isn't comparable simply wouldn't have that method. This is how the standard library gives Vec methods that appear only when the elements support them: a bounded impl.
Bounds make the error message better, too
A subtle benefit: bounds move errors to the right place. With largest<T: PartialOrd>, if you call it with a non-comparable type, the error points at your call ("this type doesn't implement PartialOrd"), which is where the mistake is. Without the generic, you'd get a confusing error deep inside the function body. Bounds are part of the function's contract (lesson 13.8): the signature now states the requirement, so the caller is told upfront what kinds of T are allowed, exactly as a parameter type tells them what values are allowed.
Quiz time
Question #1
What does the bound in fn largest<T: PartialOrd>(...) promise, and why does it make the a > b in the body legal?
Show solution
It promises that T implements PartialOrd, the trait for comparison operators, so every type largest is called with can be compared with >. That justifies a > b in the body: the compiler knows the operation is available for any allowed T, and it checks at each call site that the actual type implements PartialOrd.
Question #2
Write the bound for a generic function whose T must be both printable with {} and cloneable. Show both the inline form and the where form.
Show solution
Inline: fn f<T: std::fmt::Display + Clone>(x: T) { ... }. With a where clause:
fn f<T>(x: T)
where
T: std::fmt::Display + Clone,
{ ... }
Both require T: Display + Clone. Multiple traits are joined with +; the where form is preferred when the bounds are numerous or long.
Question #3
How does a trait bound improve the error message when a function is misused, compared to a non-generic function with the operation buried inside?
Show solution
The bound is part of the signature, so calling the function with a type that doesn't satisfy the bound produces an error at the call site ("this type doesn't implement the required trait"), pointing right at the mistake. Without the bound stated up front, the error would surface deep in the function body where the operation is used, which is harder to connect back to the bad call.
Sometimes writing <T: Trait> is more ceremony than a simple case deserves. The next lesson covers impl Trait, a lighter syntax for "some type that implements this trait," useful in both argument and return position.