Generics

Introduction to Generic Programming

Generic programming is a style of writing functions, data structures, and traits that operate on abstract type placeholders rather than single concrete types. In Rust, those placeholders are called type parameters, written inside angle brackets (<T>) and replaced with concrete types such as i32, f64, or String when the code is compiled.

Eliminating Type-Specific Duplication

Without generics, performing the same structural operation across multiple types requires duplicating the function or data structure for each concrete type.

fn first_i32(slice: &[i32]) -> Option<&i32> {
    if slice.is_empty() {
        None
    } else {
        Some(&slice[0])
    }
}

fn first_str<'a>(slice: &'a [&'a str]) -> Option<&'a &'a str> {
    if slice.is_empty() {
        None
    } else {
        Some(&slice[0])
    }
}

fn main() {
    let numbers = [10, 20, 30];
    let words = ["rust", "cargo"];

    println!("{:?}", first_i32(&numbers));
    println!("{:?}", first_str(&words));
}
Some(10)
Some("rust")

Replacing the concrete element type with a type parameter T consolidates both functions into a single generic definition while preserving full static type checking:

fn first<T>(slice: &[T]) -> Option<&T> {
    if slice.is_empty() {
        None
    } else {
        Some(&slice[0])
    }
}

fn main() {
    let numbers = [10, 20, 30];
    let words = ["rust", "cargo"];

    println!("{:?}", first(&numbers));
    println!("{:?}", first(&words));
}
Some(10)
Some("rust")

Where Generics Appear in Rust

Rust supports generic parameters across five primary language constructs:

Construct Syntax Example Purpose
Function fn swap<T>(a: &mut T, b: &mut T) Algorithm parameterized over input or output types
Struct struct Pair<T> { left: T, right: T } Record whose fields hold values of type T
Enum enum Option<T> { Some(T), None } Tagged union whose variants carry payloads of type T
Implementation block impl<T> Pair<T> { ... } Methods or trait implementations defined for all T
Trait trait From<T> { fn from(value: T) -> Self; } Interface parameterized over an external type T

In addition to type parameters (T), angle brackets can also declare lifetime parameters ('a) and const generic parameters (const N: usize):

#[derive(Debug)]
struct FixedBuffer<'a, T, const N: usize> {
    label: &'a str,
    items: [T; N],
}

fn main() {
    let buf = FixedBuffer {
        label: "rgb",
        items: [255_u8, 128, 0],
    };
    println!("{}: {:?}", buf.label, buf.items);
}
rgb: [255, 128, 0]

Early Type Checking of Generic Definitions

Unlike C++ templates, which defer type checking until a template is instantiated with a concrete type, Rust type-checks a generic function or struct body against its declared trait bounds at definition time. A generic body can only perform operations that are guaranteed to be valid for every possible type satisfying its bounds.

// ❌ does not compile
fn print_value<T>(value: T) {
    println!("{value}");
}

fn main() {
    print_value(42);
}
error[E0277]: `T` doesn't implement `std::fmt::Display`
 --> src/main.rs:2:15
  |
2 |     println!("{value}");
  |               ^^^^^^^ `T` cannot be formatted with the default formatter
  |
help: consider restricting type parameter `T` with trait `Display`
  |
1 | fn print_value<T: std::fmt::Display>(value: T) {
  |                 +++++++++++++++++++

Adding the required std::fmt::Display bound makes the contract explicit in the signature and allows the body to compile:

use std::fmt::Display;

fn print_value<T: Display>(value: T) {
    println!("{value}");
}

fn main() {
    print_value(42);
    print_value("generic string");
}
42
generic string

Zero-Cost Abstraction Model

Rust compiles generic code through monomorphization: at compile time, the compiler generates a specialized machine-code version of each generic item for every concrete type combination used in the program.

Generic Source                 Compiler Monomorphization (LLVM IR)
+------------------------+     +-----------------------------------+
| fn id<T>(x: T) -> T    | --> | fn id_i32(x: i32) -> i32          |
|                        | --> | fn id_f64(x: f64) -> f64          |
+------------------------+     +-----------------------------------+

Because specialization happens before code generation, generic functions and structs incur zero runtime type-checking overhead, zero boxing overhead, and zero virtual dispatch cost compared to hand-written concrete equivalents.