Skip to content

Intermediate · Modules, crates, traits and generics · topic 29 of 50 · 30 min

Generics and trait bounds

Explanation

Generics let you write one function or struct that works with many types, using a type parameter like T. Trait bounds (T: PartialOrd + Copy) say what T must be able to do. impl Trait is a short way to write a bound. The compiler makes a separate copy for each concrete type used (monomorphization), so generics cost nothing at run time.

Why generics?

Imagine writing largest_i32, largest_f64 and largest_char: three functions with the same logic and different types. Generics remove this duplication. A generic is a placeholder for a type, written in angle brackets, usually named T (for "type"). You have already used generics: Vec<T>, Option<T>, Result<T, E> and HashMap<K, V>.

rust
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut big = list[0];
    for &item in list {
        if item > big {
            big = item;
        }
    }
    big
}

fn main() {
    println!("{}", largest(&[34, 50, 25, 100, 65]));   // 100
    println!("{}", largest(&[1.5, 0.2, 9.9]));          // 9.9
    println!("{}", largest(&['y', 'm', 'a', 'q']));     // y
}
One function, three types. It panics on an empty slice (list[0]); a safer version would return Option<T>.

Trait bounds: what T must be able to do

Inside largest we compare with > and copy values out. Not every type can do that. A trait bound limits T to types that implement certain traits. T: PartialOrd + Copy means "T can be compared and copied". Without the bound, the compiler says "binary operation > cannot be applied to type T" and suggests the bound.

Think of it like this: A trait bound is like a job advert: "Wanted: driver. Must have a licence." The function does not care who you are, only that you have the ability it needs. T: PartialOrd is "must be able to compare".

Three ways to write bounds

rust
// 1. inline
fn show<T: std::fmt::Display>(x: T) { println!("{x}"); }

// 2. where clause: easier to read with many bounds
fn report<T, U>(a: T, b: U) -> String
where
    T: std::fmt::Display + Clone,
    U: std::fmt::Debug,
{
    format!("{a} / {:?}", b)
}

// 3. impl Trait in argument position (short form of 1)
fn show2(x: impl std::fmt::Display) { println!("{x}"); }
All three are generic functions. Use whichever is clearest.

Generic structs and impl blocks

rust
#[derive(Debug)]
struct Pair<T> {
    a: T,
    b: T,
}

impl<T: PartialOrd + std::fmt::Display> Pair<T> {
    fn bigger(&self) -> &T {
        if self.a >= self.b { &self.a } else { &self.b }
    }
}

fn main() {
    let p = Pair { a: 7, b: 12 };
    println!("{}", p.bigger());     // 12
}
impl<T> ... Pair<T> declares T for the methods. The bound limits which Pairs get the method.

impl Trait in return position

fn make_counter() -> impl Iterator<Item = u32> says "I return some type that is an iterator of u32; you do not need its exact name". This is very useful for iterators and closures, whose real types are long or impossible to write. The function must still return one single concrete type.

Monomorphization: zero-cost generics

When you call largest with i32 and with f64, the compiler generates two specialised copies, largest::<i32> and largest::<f64>, as if you had written them by hand. This is called monomorphization. The result runs as fast as hand-written code. The cost is compile time and a larger program file when a generic is used with many types.

output
largest(&[1, 2])    -> compiler makes  fn largest_i32(list: &[i32]) -> i32
largest(&[1.0, 2.0]) -> compiler makes  fn largest_f64(list: &[f64]) -> f64
A picture of what happens behind the scenes.

Turbofish

Sometimes the compiler cannot guess T. Then you name it with the "turbofish" syntax ::<>, which you met with parse::<u32>() and collect::<Vec<_>>().

Common mistakes

  • Missing bounds: using > or {} on T without PartialOrd or Display. Add the bound the compiler suggests.
  • Too many bounds "just in case". Ask only for what the function really uses.
  • Returning different types from an impl Trait function (one branch a Vec iterator, another a range). It must be one concrete type; use Box<dyn Trait> for that case (next topic).
  • Forgetting <T> after impl: write impl<T> Pair<T>, not impl Pair<T>.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Generics and trait bounds · 1/6

Vec<T> Option<T> Result<T, E>

Generics let one piece of code work with many types. You have used them already: Vec, Option and Result.

Prefer a video by a teacher? Find free videos on “rust generics trait bounds where clause monomorphization” on YouTube ↗

In real life: A generic paginated API response

A shopping API returns pages of products, pages of orders and pages of reviews. Instead of three almost identical structs, the team writes one generic Page<T> with items, page number and total. Each endpoint returns Page<Product>, Page<Order> or Page<Review>, and the same helper methods work for all of them.

rust
#[derive(Debug)]
struct Page<T> {
    items: Vec<T>,
    page: u32,
    total_pages: u32,
}

impl<T> Page<T> {
    fn has_next(&self) -> bool {
        self.page < self.total_pages
    }
}

fn main() {
    let p = Page { items: vec!["kurta", "saree"], page: 1, total_pages: 3 };
    println!("{} items, next page? {}", p.items.len(), p.has_next());
}
Prints: 2 items, next page? true

Practice (0/3 done)

Task 1 · Question

Why does this not compile? fn show<T>(x: T) { println!("{}", x); }

Task 2 · Question

What does monomorphization mean?

Task 3 · Practice

Write a generic fn count_matching<T: PartialEq>(items: &[T], target: &T) -> usize. Use it with numbers and with &str values. Then write a generic struct Stack<T> with push, pop -> Option<T> and is_empty methods, and use it with u32 and String.

Quick quiz

Question 1

1.What is T in fn f<T>(x: T)?

Question 2

2.What does T: PartialOrd + Copy mean?

Question 3

3.Are generics slower than hand-written code for each type?

Question 4

4.What does -> impl Iterator<Item = u32> promise?

Question 5

5.Which is the turbofish syntax?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is T in fn f<T>(x: T)?

Answer: A type parameter (placeholder for a type)

T stands for any type that meets the bounds.

What does T: PartialOrd + Copy mean?

Answer: T must be both comparable and copyable

+ combines several required traits.

Are generics slower than hand-written code for each type?

Answer: No, monomorphization makes them as fast

Specialised copies are generated at compile time.

What does -> impl Iterator<Item = u32> promise?

Answer: The function returns some single concrete type that is an iterator of u32

impl Trait in return position hides one concrete type.

Which is the turbofish syntax?

Answer: parse::<u32>()

::<> names a generic type explicitly.

What is a trait?Related

Answer: A set of methods types can promise to implement

Traits describe shared behaviour.

From Traits: shared behaviour

What do generics mainly give you?Related

Answer: Type safety at compile time without casts

Errors move from run time to compile time.

From Generics

Why must dyn Trait be behind a pointer like Box?Related

Answer: Its size is not known at compile time

Different implementing types have different sizes.

From Trait objects and dynamic dispatch

What is Self inside impl Wallet?Related

Answer: A short name for the type Wallet

Self (capital S) refers to the type; self (small s) to the instance.

From Methods and associated functions

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Modules, crates, traits and generics” before its exam: Trait objects and dynamic dispatch.
  2. Module 7 exam: Modules, crates, traits and generics · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Closures, iterators and smart pointers. 50 topics left in the course before the final exam.

← Previous: Traits: shared behaviour