Bounds, where clauses and impl Trait
In Rust an unconstrained type parameter can be moved and stored, and that is all. To print a T, compare it or clone it, the function has to say which trait it needs, and that statement is the bound. The compiler then checks every call and every body against the promise.
This is the part of generics that differs most from a C++ template: the promise is written in the signature and checked at the definition, not discovered at the instantiation. The lesson covers the simple bound, several bounds, the where clause and the impl Trait shorthand, and ends with the error for a missing one.
A bound on T
fn show<T: Display>(value: T) may use the formatting placeholder because the bound promises that the type implements Display. The same function works for every type that keeps the promise.
The program prints an integer and a string slice through one function. Both types implement Display, and the compiler checks that at the two call sites.
use std::fmt::Display;
fn show<T: Display>(value: T) {
println!("value: {value}");
}
fn main() {
show(42);
show("rust");
}
rustc --edition 2024 bound.rs
./bound
value: 42
value: rust
The bound is part of the public API: a caller reads the signature and knows what the function needs. The lesson on traits builds the trait that a bound names.
Two bounds
Several bounds are joined with a plus: T: Display + Clone promises both operations. The body may then format the value and clone it, and a type that implements only one of the two is rejected.
The program prints the value twice by cloning it. The output shows the same word and the same number on one line, which is the clone working.
use std::fmt::Display;
fn show_twice<T: Display + Clone>(value: T) {
println!("{value} {value}");
}
fn main() {
show_twice("rust");
show_twice(7);
}
rustc --edition 2024 multiple.rs
./multiple
rust rust
7 7
The order of the bounds does not matter, and the compiler reports the first one that is missing at a call site. A long list is easier to read in a where clause, which is the next section.
A where clause
The where clause moves the bounds below the signature, which keeps a long parameter list readable. where T: PartialOrd + Debug is the same promise as the angle-bracket form, and the two may be mixed.
The program finds the largest value of a slice and prints it with the debug form. The bound allows both the comparison and the print.
use std::fmt::Debug;
fn largest<T>(values: &[T]) -> &T
where
T: PartialOrd + Debug,
{
let mut best = &values[0];
for value in values {
if value > best {
best = value;
}
}
best
}
fn main() {
println!("{:?}", largest(&[3, 9, 4]));
}
rustc --edition 2024 where_clause.rs
./where_clause
9
The reference returned by the function borrows the slice, which is why the signature has one input lifetime and no output annotation. The lesson on lifetimes covers that rule.
impl Trait
impl Display in a parameter position is sugar for a generic parameter with one bound. In a return position it means one concrete type that implements the trait, chosen by the function, and the caller cannot name it.
The program wraps a value in brackets and prints the result. The return type is impl Display, and the concrete type here is a String.
use std::fmt::Display;
fn wrap(value: impl Display) -> impl Display {
format!("[{value}]")
}
fn main() {
println!("{}", wrap(42));
println!("{}", wrap("rust"));
}
rustc --edition 2024 impl_trait.rs
./impl_trait
[42]
[rust]
The return form keeps the type hidden but still known to the compiler, so the call has no vtable. It also lets a library change the concrete type without breaking a caller, because the caller never wrote it down. The trade is that the caller cannot use any method that the trait does not declare. The lesson on trait objects shows the boxed version, which can return different types on different paths.
A missing bound
Without the bound, the formatting placeholder has nothing to call. The error is E0277, and the message names T, the trait it needs and the line in the body, so the fix is one bound in the signature.
The program below prints a T with no promise. The compiler reports that T does not implement Display, which is exactly the missing sentence in the signature.
fn show<T>(value: T) {
println!("value: {value}");
}
fn main() {
show(42);
}
rustc --edition 2024 missing_bound_err.rs
error[E0277]: `T` doesn't implement `std::fmt::Display`
--> missing_bound_err.rs:2:22
|
2 | println!("value: {value}");
| ^^^^^^^ `T` cannot be formatted with the default formatter
|
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
help: consider restricting type parameter `T` with trait `Display`
|
1 | fn show<T: std::fmt::Display>(value: T) {
| +++++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
The call is fine; the definition is incomplete. That direction is the useful one: a generic body is checked once, and every call is checked against the bounds it declares.
Exercises
- Write a function that returns the largest of two values. Which bound does the comparison need?
- Move the bounds of the where-clause program into the angle brackets and compare the two signatures.
- Change the impl Trait return to a boxed trait object. Why does the caller not need to change?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a trait bound on T promise?
Answer
That T implements the trait, so its methods may be used
The bound is the permission to call the trait methods inside the generic body.
-
How are two bounds written in angle brackets?
Answer
T: Display + Clone
The plus joins bounds, and each one is checked at the call site.
-
What does a where clause change?
Answer
It moves the bounds below the signature for readability
The meaning is the same; only the position in the source changes.
-
What does
impl Displayin a return position mean?Answer
One concrete type that implements Display, chosen by the function
The function decides the type; the caller only knows the trait.
-
What does this program print?
use std::fmt::Display; fn show<T: Display>(value: T) { println!("value: {value}"); } fn main() { show(42); show("rust"); }Answer
value: 42 / value: rust
The function is instantiated for each type, and both calls print through Display.
-
What does this program print?
use std::fmt::Display; fn show_twice<T: Display + Clone>(value: T) { println!("{value} {value}"); } fn main() { show_twice("rust"); show_twice(7); }Answer
rust rust / 7 7
The clone duplicates the value, and the display prints it twice on each line.
-
What does this program print?
fn largest<T>(values: &[T]) -> &T where T: PartialOrd, { let mut best = &values[0]; for value in values { if value > best { best = value; } } best } fn main() { println!("{}", largest(&[3, 9, 4])); }Answer
9
The comparison finds the largest value, which is nine.
-
What does this program print?
use std::fmt::Display; fn wrap(value: impl Display) -> impl Display { format!("[{value}]") } fn main() { println!("{}", wrap(42)); }Answer
[42]
The function builds a String with brackets around the formatted value.
-
What is the first line of the compiler output for this program?
fn show<T>(value: T) { println!("value: {value}"); } fn main() { show(42); }Answer
error[E0277]:
Tdoesn't implementstd::fmt::DisplayThe placeholder needs Display, and the body has no bound that promises it.
-
What does this program print?
fn bigger<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } } fn main() { println!("{}", bigger(3, 9)); println!("{}", bigger("a", "b")); }Answer
9 / b
The comparison picks the larger value for integers and for strings, which are ordered.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev