Generics

Generics let you write code that works with many types instead of just one, without giving up type safety or runtime performance. Instead of writing a separate largest_i32, largest_f64, and largest_char function, you write a single largest<T> function once, and the compiler generates the specialized versions for you. This is one of Rust’s core tools for eliminating duplication while keeping every guarantee the borrow checker and type system provide.

Overview: How Generics Work

A generic is a placeholder for a type that gets filled in later. When you write fn largest<T>(list: &[T]) -> T, T is not a real type — it is a stand-in that means "some type, to be determined by whoever calls this function." The angle brackets <T> declare that this function is generic over a type parameter named T. Inside the function body, you can use T anywhere you’d use a concrete type: as a parameter type, a return type, or a field type.

The crucial question is: what can you actually do with a value of type T inside the function? By default, almost nothing — the compiler has no idea what T will end up being, so it refuses to let you compare two T values with >, print one with {}, or copy one, because not every possible type supports those operations. This is where trait bounds come in: writing T: PartialOrd tells the compiler "whatever T turns out to be, it must implement the PartialOrd trait," which unlocks the > and < operators for that type inside the function. Bounds are a contract — the compiler checks the body of your generic function against exactly the capabilities the bounds promise, nothing more.

Under the hood, Rust generics are not like Java generics (which erase type information at runtime) or a dynamically-typed language’s duck typing. Rust uses monomorphization: at compile time, for every distinct concrete type your generic code is actually called with, the compiler generates a separate, fully specialized copy of that function or struct, as if you had hand-written it for that one type. Calling largest(&some_vec_of_i32) and largest(&some_vec_of_char) causes the compiler to emit two independent functions, largest_i32 and largest_char in spirit (the real names are mangled), each compiled and optimized exactly like non-generic code. The generic function itself never exists in the compiled binary — only its concrete instantiations do. This is why Rust generics are described as "zero-cost abstractions": there is no runtime dispatch, no boxing, and no performance penalty compared to writing the type-specific code by hand. The tradeoff is longer compile times and larger binaries when a generic is instantiated with many different types, since each instantiation duplicates the machine code.

Generics apply to more than functions. Structs, enums, and methods (via impl blocks) can all be generic over one or more type parameters, and you can mix generics with the ownership and borrowing rules you already know — a generic struct still has exactly one owner for each field, and a generic function’s borrow checking works exactly the same way it would for a concrete type.

Syntax

The general shapes you’ll use for generic items look like this:

fn function_name<T: Trait1 + Trait2>(param: T) -> T {
    // body uses only what Trait1 and Trait2 guarantee
}

struct StructName<T> {
    field: T,
}

impl<T: Trait> StructName<T> {
    // methods
}

enum EnumName<T> {
    Variant1(T),
    Variant2,
}

fn generic_where<T>(param: T) -> T
where
    T: Trait1 + Trait2,
{
    // equivalent to the inline bound above, easier to read
    // when there are several type parameters and bounds
}
Piece Meaning
<T> Declares a type parameter named T. Any identifier works, but single uppercase letters (T, U, K, V) are the convention.
T: Trait1 + Trait2 A trait bound: restricts T to types implementing both traits, and grants the function body those traits’ methods/operators.
where T: Trait An alternate, more readable place to write bounds, especially with multiple parameters or complex bounds.
impl<T> StructName<T> Implements methods for every possible T; the <T> after impl must be declared before it’s used after the struct name.
StructName<T, U> Multiple independent type parameters — the fields can hold different concrete types.

Examples

Example 1: A Generic Function

This function finds the largest item in a slice of any type that can be compared and copied:

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

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    let result = largest(&numbers);
    println!("The largest number is {}", result);

    let chars = vec!['y', 'm', 'a', 'q'];
    let result = largest(&chars);
    println!("The largest char is {}", result);
}

Output:

The largest number is 100
The largest char is y

The bound T: PartialOrd + Copy is doing two jobs. PartialOrd unlocks the > comparison inside the loop; Copy allows list[0] to be copied out of the borrowed slice into largest instead of trying to move a value out of something you only borrowed (which the borrow checker would reject). Because i32 and char both implement PartialOrd and Copy, the same function body works for both without any code duplication — the compiler generates one specialized version per call site’s type.

Example 2: A Generic Struct

struct Point<T> {
    x: T,
    y: T,
}

impl<T: std::fmt::Display> Point<T> {
    fn describe(&self) -> String {
        format!("({}, {})", self.x, self.y)
    }
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.5, y: 2.5 };

    println!("Integer point: {}", integer_point.describe());
    println!("Float point: {}", float_point.describe());
}

Output:

Integer point: (5, 10)
Float point: (1.5, 2.5)

Point<T> is one struct definition that can hold an i32 pair, an f64 pair, or a pair of any other type — but within a single instance, x and y must be the same concrete type, because both fields share the one type parameter T. The impl<T: std::fmt::Display> block only provides describe for types that implement Display; a hypothetical Point holding a type without Display would simply not have a describe method available.

Example 3: A Generic Stack

A more realistic use of generics: a reusable last-in-first-out container.

struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { items: Vec::new() }
    }

    fn push(&mut self, item: T) {
        self.items.push(item);
    }

    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

fn main() {
    let mut stack: Stack<String> = Stack::new();
    stack.push(String::from("first"));
    stack.push(String::from("second"));
    stack.push(String::from("third"));

    while !stack.is_empty() {
        match stack.pop() {
            Some(value) => println!("Popped: {}", value),
            None => println!("Stack is empty"),
        }
    }
}

Output:

Popped: third
Popped: second
Popped: first

Stack<T> has no trait bounds at all — push, pop, and is_empty only move values in and out of a Vec<T>, which every type can do, so no bound is required. Notice pop returns Option<T>, not T directly: popping an empty stack is a normal, expected case in Rust, not an error, so the possibility of "nothing there" is encoded in the type itself rather than risking a runtime panic or a null value.

How It Works Step by Step: Monomorphization

Walk through what the compiler does with Example 1’s two calls to largest:

  1. The compiler sees largest(&numbers) where numbers: Vec<i32>, so it infers T = i32 for this call site.
  2. It checks that i32 actually implements the bounds PartialOrd and Copy — it does, so the call is allowed.
  3. The compiler generates a concrete version of largest with every T replaced by i32, as if you had written fn largest_i32(list: &[i32]) -> i32 by hand.
  4. It sees largest(&chars) where chars: Vec<char>, infers T = char, checks the same bounds against char, and generates a second, independent concrete version specialized for char.
  5. Each concrete version is compiled and optimized on its own, exactly like ordinary non-generic code — there is no shared "generic" machine code and no runtime type-checking or dispatch.

The practical upshot: a generic function call has identical performance to a hand-written, type-specific function. The cost is paid entirely at compile time (more code to generate and optimize), never at runtime.

Common Mistakes

Mistake 1: Missing Trait Bounds

Writing a generic function that uses an operator or method without declaring the trait that provides it fails to compile, because the compiler cannot assume any type supports it:

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

This fails with errors like "binary operation > cannot be applied to type T" and "cannot move out of index of [T]," because without bounds, T could be any type at all — including one that has no ordering and cannot be copied. The fix is to add the bounds the body actually needs, as shown in Example 1:

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

Mistake 2: Assuming One Type Parameter Covers Different Types

A single type parameter used for two fields forces both fields to be the same concrete type in any given instance:

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

The compiler infers T = i32 from x: 5, then rejects y: 4.0 with a "mismatched types" error, because 4.0 is an f64, not an i32. This surprises newcomers who expect generics to mean "any type, independently, per field." If the fields genuinely need to differ, declare a second type parameter:

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let mixed = Point { x: 5, y: 4.0 };
    println!("x = {}, y = {}", mixed.x, mixed.y);
}

Now x is inferred as i32 and y as f64 independently, because they’re tied to separate type parameters.

Best Practices

  • Only add the trait bounds a generic function’s body actually requires — extra bounds needlessly restrict which types can call it.
  • Prefer where clauses over long inline bound lists once you have more than one type parameter or more than one or two bounds, for readability.
  • Use &T parameters instead of requiring T: Copy when your function only needs to read the value, so it also works with non-Copy types like String or Vec<T>.
  • Remember generics are resolved at compile time: you cannot store a Vec of "any T" at runtime without a different mechanism (like trait objects with dyn Trait), since a generic collection is monomorphized to one concrete type per instantiation.
  • Reach for a second type parameter (<T, U>) whenever two fields or parameters are conceptually unrelated types, rather than forcing an artificial shared type.
  • Keep an eye on compile times and binary size in large codebases with heavily generic APIs — every distinct instantiation duplicates code.

Practice Exercises

  1. Write a generic function smallest<T: PartialOrd + Copy>(list: &[T]) -> T that returns the smallest item in a slice, and call it with both a Vec<i32> and a Vec<f64>.
  2. Define a generic struct Pair<T> with two fields first: T and second: T, and write a method swap(&mut self) that swaps the two fields in place. (Hint: std::mem::swap can swap two mutable references.)
  3. Extend the Stack<T> from Example 3 with a method peek(&self) -> Option<&T> that returns a reference to the top item without removing it. Expected output when peeking a stack containing "only": Some("only") when printed with {:?}.

Summary

  • Generics let one function, struct, enum, or method definition work across many concrete types.
  • Type parameters like T are placeholders with no capabilities by default; trait bounds (T: Trait) grant the operations and methods the body needs.
  • Rust compiles generics via monomorphization: a separate, fully concrete copy is generated for each type actually used, giving zero runtime cost compared to hand-written type-specific code.
  • A struct with one shared type parameter forces all fields using it to be the same concrete type in a given instance; use multiple type parameters when fields should vary independently.
  • Generic code still obeys every ownership and borrowing rule — bounds like Copy often exist specifically to satisfy those rules inside the generic body.
  • Only require the bounds you actually use, and prefer where clauses for readability as bounds grow.