RustArena

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

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

Practice it