Coding by Hand
Rust home

Generics and Bounds

A generic function in Rust is a master die in a stamping press. You carve one shape into hardened steel, you bolt it to the press, and then you can feed in different metal blanks — copper, brass, aluminum — and the press stamps out a finished part for each one. The die never bends. It just runs again. The letter T you see inside angle brackets is the empty slot in the die where the metal goes. The compiler is the press operator who slides each type into the slot and produces a real, type-specific copy at build time.

A factory stamping press: one steel die produces a specialized part for each metal blank fed in.
A factory stamping press: one steel die produces a specialized part for each metal blank fed in.

The idea is old. David Musser and Alex Stepanov were sitting at HP Labs in 1992 trying to write a sort function in C++ that could handle integers, strings, and user-defined records without copying the source three times. They built the Standard Template Library around the idea of "containers and algorithms parameterized by type," and shipped it as part of C++ in 1994. Bjarne Stroustrup's term for it was "generic programming." Java got a thinner version in 2004 — Java generics throw the type information away at runtime, which is called erasure. Rust took the C++ approach and ran it through the borrow checker. When Graydon Hoare and Niko Matsakis put generics into Rust, they kept the rule that no runtime cost should appear out of nowhere. The press stamps real copies. There is no boxing, no virtual dispatch, no hidden table lookup.

Here is the die. One function, one slot for the type.

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

Read it slowly. The <T: PartialOrd + Copy> after the function name is two things at once. The T introduces a type slot the caller will fill. The colon-and-plus part is the checklist taped above the press — a list of operations the type must support before the press will accept it. PartialOrd says "this type knows how to answer >." Copy says "this type can be duplicated by copying its bits, no ownership move needed." Without those bounds the body would not compile, because the line if item > winner calls >, and the line winner = item needs to move the value without taking the slice's ownership. The checklist is how the compiler proves the body is safe for every type that could ever land in the slot.

The trait-bounds checklist taped above the press: each operation the body uses must be checked off.
The trait-bounds checklist taped above the press: each operation the body uses must be checked off.

Drop the PartialOrd bound for a second and you can see what the press refuses to do. Without it, T is just "some type the caller picks" and > is no longer guaranteed to exist. The compiler stops with error[E0369]: binary operation > cannot be applied to type T. The press refuses to run because the metal blank you handed it might not be one it knows how to stamp. Add PartialOrd back and the operation appears in the toolkit again. The bounds are not bureaucracy. They are the contract between the function body and every caller.

Watch the press in action. Same die, two different metals — a slice of integers and a slice of characters.

fn main() {
    let scores = [42, 17, 99, 8, 73];
    let letters = ['r', 'u', 's', 't'];

    println!("press stamps one copy of largest for i32:");
    println!("  scores  = {scores:?}");
    println!("  largest = {}", largest(&scores));
    println!();

    println!("press stamps a second copy of largest for char:");
    println!("  letters = {letters:?}");
    println!("  largest = {}", largest(&letters));
    println!();

    let coin_toss = Pair {
        first: 7,
        second: 3,
    };
    let race = Pair {
        first: 'k',
        second: 'z',
    };
    println!("Pair<i32>  bigger = {}", coin_toss.bigger());
    println!("Pair<char> bigger = {}", race.bigger());
    println!();

    println!("where-clause version, same checklist:");
    println!("  show_bigger(10, 4)   = {}", show_bigger(10, 4));
    println!("  show_bigger('a','m') = {}", show_bigger('a', 'm'));
    println!();

    stamping_press();
}

Run it, and you see one stamped output for i32 and one for char, both produced by the same source.

press stamps one copy of largest for i32:
  scores  = [42, 17, 99, 8, 73]
  largest = 99

press stamps a second copy of largest for char:
  letters = ['r', 'u', 's', 't']
  largest = u

Pair<i32>  bigger = 7
Pair<char> bigger = z

where-clause version, same checklist:
  show_bigger(10, 4)   = 10
  show_bigger('a','m') = m

generic source (1 copy)         monomorphized output (1 per type)
+---------------------------+   +---------------------------+
| fn largest<T>(list:&[T])  |   | fn largest_i32(&[i32]) -> |
|   where T: PartialOrd     |-->|   i32 { ... uses > ... } |
|         + Copy            |   +---------------------------+
| { ... item > winner ... } |   +---------------------------+
+---------------------------+   | fn largest_char(&[char])  |
        |                  +--->|   -> char { ... > ... }  |
        |                       +---------------------------+
        v
checklist the compiler reads:
  [x] T: PartialOrd  -- can use >
  [x] T: Copy        -- can move winner without taking ownership

The thing the output is not showing you is the compiler doing real work behind the scenes. This is called monomorphization — a Greek word that means "one shape," but it ends up meaning the opposite. When cargo build sees largest(&scores) with scores: [i32; 5], it stamps out a private copy of largest with every T replaced by i32. When it sees largest(&letters) with chars, it stamps a second copy. Each copy is its own machine-code function, hardcoded for that one type. The runtime cost of the generic is zero — calling largest(&scores) is exactly as fast as if you had written a non-generic fn largest_i32(list: &[i32]) -> i32. The trade is binary size. If you call largest on 8 different types, you get 8 stamped functions inside the executable. Most programs only ever call any one generic on 2 or 3 types, so the trade pays off everywhere it shows up.

fn stamping_press() {
    println!("generic source (1 copy)         monomorphized output (1 per type)");
    println!("+---------------------------+   +---------------------------+");
    println!("| fn largest<T>(list:&[T])  |   | fn largest_i32(&[i32]) -> |");
    println!("|   where T: PartialOrd     |-->|   i32 {{ ... uses > ... }} |");
    println!("|         + Copy            |   +---------------------------+");
    println!("| {{ ... item > winner ... }} |   +---------------------------+");
    println!("+---------------------------+   | fn largest_char(&[char])  |");
    println!("        |                  +--->|   -> char {{ ... > ... }}  |");
    println!("        |                       +---------------------------+");
    println!("        v");
    println!("checklist the compiler reads:");
    println!("  [x] T: PartialOrd  -- can use >");
    println!("  [x] T: Copy        -- can move winner without taking ownership");
}

The diagram from the program drives the picture home. One block of source on the left, two blocks of compiled output on the right, and the checklist underneath confirming that every operation in the body has a matching bound. The press cannot be tricked. If you try to call largest on a type that does not implement PartialOrd — say, a custom struct you wrote yourself with no comparison — the compiler refuses at the call site with a complaint that names the missing trait.

Structs can hold generics too. The same <T> syntax that opens a slot in a function opens a slot in a struct.

struct Pair<T> {
    first: T,
    second: T,
}

impl<T: PartialOrd + Copy> Pair<T> {
    fn bigger(&self) -> T {
        if self.first >= self.second {
            self.first
        } else {
            self.second
        }
    }
}

A Pair<T> is a struct with two fields of the same type. The impl<T: PartialOrd + Copy> Pair<T> block hangs methods onto every Pair<T> whose T passes the checklist. The press stamps one bigger method for Pair<i32>, another for Pair<char>, and refuses to stamp one for Pair<TcpStream> because TcpStream is neither PartialOrd nor Copy. Notice the bounds live on the impl block, not on the struct. The struct itself does not need to compare its fields — only the method that does the comparing does.

When the checklist grows past 2 or 3 bounds, the angle-bracket line stops fitting on one screen. Niko Matsakis added a second syntax for the same idea, called a where clause. It moves the bounds out of the angle brackets and lists them after the return type. The function still works the same way — same press, same die, same stamped output — but the signature stays readable.

fn show_bigger<T>(a: T, b: T) -> T
where
    T: PartialOrd + Copy,
{
    if a >= b { a } else { b }
}

Try this — what would happen if you handed largest an empty slice? The function reaches for list[0] first thing, which would panic with an index-out-of-bounds error. There is no bound the compiler can add to prevent that, because "non-empty slice" is not a property the type system tracks. You would have to handle it in the body — return an Option<T> instead — and that's the next bottleneck: bounds can promise an operation exists, but they cannot promise the data shape the operation needs, which is where trait objects and dynamic dispatch come in.