Generics in Rust: generic functions, structs and trait bounds
How to write generic functions and structs in Rust, add trait bounds like T: PartialOrd, use where clauses, and why generics cost nothing at run time.
Generics let you write one function or type that works for many types. Instead of largest_i32, largest_f64 and largest_char, you write largest<T> once.
Generic functions and trait bounds
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut best = &items[0];
for item in items {
if item > best {
best = item;
}
}
best
}
fn main() {
println!("{}", largest(&[3, 9, 2]));
println!("{}", largest(&[1.5, 0.2]));
println!("{}", largest(&["kiwi", "apple", "pear"]));
}
9
1.5
pear
<T> declares a type parameter. T: PartialOrd is a trait bound: "T can be any type that supports comparison with < and >". Without the bound, the compiler refuses to compare two Ts, because it can't assume every type can be compared:
fn largest<T>(items: &[T]) -> &T {
let mut best = &items[0];
for item in items {
if item > best {
best = item;
}
}
best
}
fn main() {
println!("{}", largest(&[1, 5, 2]));
}
error[E0369]: binary operation `>` cannot be applied to type `&T`
rustc's help line suggests the fix: "consider restricting type parameter T with trait PartialOrd". This is the core idea: inside a generic function you can only use what the bounds promise.
Several bounds combine with + (T: Display + Clone). When they get long, move them to a where clause:
fn report<T, U>(item: &T, extra: U) -> String
where
T: std::fmt::Display + Clone,
U: Into<String>,
{ ... }
Generic structs and enums
struct Pair<T> {
left: T,
right: T,
}
impl<T: PartialOrd + Copy> Pair<T> {
fn max(&self) -> T {
if self.left > self.right { self.left } else { self.right }
}
}
impl<T> Pair<T> declares T again for the impl. You can also write methods for one concrete type only: impl Pair<f64> { fn distance(&self) -> f64 { ... } }.
You already use generic types all the time: Vec<T>, Option<T>, Result<T, E>, HashMap<K, V>.
Zero cost: monomorphization
The compiler generates a separate copy of a generic function for each concrete type it's used with (monomorphization). largest::<i32> and largest::<f64> become two ordinary functions, so generics run exactly as fast as hand-written versions. The trade-off is compile time and binary size.
Const generics
Types can also be generic over values: fn sum<const N: usize>(arr: [i32; N]) -> i32 works for arrays of any length.
Common mistakes
- Too few bounds: E0369 or E0599 ("method not found... trait bounds were not satisfied"). Add the trait that provides the operation.
- Too many bounds: only require what the function uses; extra bounds just reject callers.
- Returning
Tby value from a slice of non-Copyitems: you can't move out of&[T]. Return&T, or add aClonebound and clone. - Making everything generic. If a function is only ever called with one type, a concrete type is simpler to read.
Example
use std::fmt::Display;
#[derive(Debug)]
struct Pair<T> {
left: T,
right: T,
}
impl<T: PartialOrd + Copy> Pair<T> {
fn new(left: T, right: T) -> Self {
Self { left, right }
}
fn max(&self) -> T {
if self.left > self.right { self.left } else { self.right }
}
}
impl Pair<f64> {
fn distance(&self) -> f64 {
(self.left - self.right).abs()
}
}
fn show_all<T>(label: &str, items: &[T]) -> String
where
T: Display,
{
let parts: Vec<String> = items.iter().map(|i| i.to_string()).collect();
format!("{label}: {}", parts.join(", "))
}
fn sum<const N: usize>(values: [i32; N]) -> i32 {
let mut total = 0;
for v in values {
total += v;
}
total
}
fn main() {
let p = Pair::new(3, 7);
let q = Pair::new('x', 'b');
let r = Pair::new(1.5, -2.0);
println!("{} {} {}", p.max(), q.max(), r.max());
println!("distance {}", r.distance());
println!("{p:?}");
println!("{}", show_all("scores", &[10, 20, 30]));
println!("{}", show_all("names", &["Ferris", "Corro"]));
println!("{} {}", sum([1, 2, 3]), sum([10; 5]));
}
Output:
7 x 1.5
distance 3.5
Pair { left: 3, right: 7 }
scores: 10, 20, 30
names: Ferris, Corro
6 50