Generics and trait bounds

Rust · Practical · 14 min

Generics let one piece of code work for many types with no runtime overhead, and you read them in every API signature.

Generics let you write code once for many types. fn largest<T: PartialOrd>(items: &[T]) -> &T works for slices of integers, floats or strings. Structs and enums can be generic too: struct Pair<T> { a: T, b: T }, and of course Vec<T>, Option<T> and Result<T, E>.

Inside a generic function you can only do what the trait bounds promise. Without T: PartialOrd you cannot compare with >; without T: Clone you cannot call .clone(). The compiler checks the generic body once against the bounds, so errors appear at the definition, not deep inside an instantiation.

Bounds can be written inline (T: Display + Clone) or in a `where` clause, which reads better when there are several:

fn report<K, V>(map: &HashMap<K, V>) where K: Display + Eq + Hash, V: Debug { }

impl blocks can be generic (impl<T> Pair<T> { }) or conditional: impl<T: Display> Pair<T> { fn show(&self) } adds methods only when T qualifies. A blanket impl like impl<T: Display> ToString for T gives a trait to every type meeting a bound.

Rust compiles generics by monomorphization: it generates a specialised copy for each concrete type used. That makes them as fast as hand-written code, at the cost of compile time and binary size. When that matters, or when you need a collection of mixed types, trait objects (dyn Trait, a later lesson) trade a little runtime speed for one shared copy.

impl Trait in argument position is shorthand for an anonymous generic parameter. Use a named T when you need to mention the type twice or let callers specify it with turbofish syntax: parse::<u32>(), collect::<Vec<_>>().

Generics also accept const parameters: fn first<T, const N: usize>(arr: [T; N]).

Example

use std::fmt::Display;

fn largest<T: PartialOrd>(items: &[T]) -> &T {
    let mut best = &items[0];
    for item in items {
        if item > best { best = item; }
    }
    best
}

struct Pair<T> { a: T, b: T }
impl<T> Pair<T> where T: Display + PartialOrd { // methods only for comparable, printable T
    fn show_max(&self) {
        let m = if self.a > self.b { &self.a } else { &self.b };
        println!("max = {m}");
    }
}
fn sum_all<const N: usize>(xs: [i32; N]) -> i32 { xs.iter().sum() }

fn main() {
    println!("{} {}", largest(&[3, 9, 2]), largest(&["kiwi", "apple"])); // 9 kiwi
    Pair { a: 1.5, b: 0.5 }.show_max();                                 // max = 1.5
    let v = "1 2 3".split(' ').map(|s| s.parse::<i32>().unwrap()).collect::<Vec<_>>();
    println!("{v:?} {}", sum_all([1, 2, 3, 4]));                        // [1, 2, 3] 10
}

Remember

  • Trait bounds define what a generic body is allowed to do with T
  • Use where clauses for long bounds; conditional impl blocks add methods per bound
  • Generics are monomorphized: zero runtime cost, more compile time and code size

Watch out for

  • Adding bounds to a struct definition instead of only to the impl blocks that need them.

Check yourself

  1. Why does `fn max<T>(a: T, b: T) -> T { if a > b { a } else { b } }` fail to compile?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch10-01-syntax.html