Polyglot
Languages Rust generics
Rust § generics

Generics

Rust generics admit code that works for many types without sacrificing performance: each generic instantiation produces specialised code, with no runtime dispatch overhead. The mechanism — monomorphisation — is the foundation of Rust’s “zero-cost abstraction” claim. Generics combine with trait bounds (constraints on which types are admitted), associated types (types determined by the trait implementation), and const generics (parameters that are values rather than types). Together, the mechanisms admit substantial type-safe code reuse with the performance of hand-specialised code.

This page covers generic functions, generic types, trait bounds, where clauses, const generics, and the conventional patterns. The relationship to traits is in Traits; the runtime model is in Ownership and borrowing.

Generic functions

A generic function takes type parameters:

fn first<T>(v: &Vec<T>) -> Option<&T> {
    v.first()
}

let n = first(&vec![1, 2, 3]);                   // T = i32
let s = first(&vec!["a", "b"]);                  // T = &str

The <T> introduces a type parameter; the function admits any T. The type is determined at the call site through inference.

For multiple parameters:

fn pair<A, B>(a: A, b: B) -> (A, B) {
    (a, b)
}

The conventional naming:

  • T for a single type parameter.
  • T1, T2, … or T, U, V for multiple parameters with no semantic distinction.
  • K, V for key/value pairs.
  • E for error types or element types.
  • R for return types.

Generic types

A struct, enum, or trait may take type parameters:

struct Pair<A, B> {
    first: A,
    second: B,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

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

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

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

The <T> after impl introduces the type parameter for the implementation block. The block’s methods are available for any Container<T>.

Trait bounds

A type parameter may be constrained with trait bounds:

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

The T: PartialOrd admits > on the type. Without the bound, the function would not compile; the compiler would not know that T admits comparison.

Multiple bounds with +:

fn show_clone<T: Display + Clone>(item: T) -> String {
    let copy = item.clone();
    format!("{}", copy)
}

The + admits multiple constraints; the function requires both Display and Clone.

where clauses

For non-trivial bounds, the where clause admits cleaner formatting:

fn complex<T, U, V>(t: T, u: U) -> V
where
    T: Display + Clone,
    U: Iterator<Item = T>,
    V: From<T>,
{
    // ...
}

The where form is the conventional choice for multi-line bounds; the inline form (<T: Trait>) is conventional for simple cases.

Generic implementations

The impl block introduces type parameters for the implementation:

struct Wrapper<T>(T);

impl<T: Display> Wrapper<T> {
    fn show(&self) {
        println!("{}", self.0);
    }
}

let w = Wrapper(42);
w.show();

The impl<T: Display> admits the implementation for any Wrapper<T> where T implements Display. Other Wrapper<T> instances (with T not implementing Display) do not have the show method.

The mechanism admits conditional implementations — methods that are available only for specific types:

impl Container<i32> {                          // specifically for Container<i32>
    fn sum(&self) -> i32 {
        self.items.iter().sum()
    }
}

impl<T: Clone> Container<T> {                  // for any T: Clone
    fn duplicate(&self) -> Vec<T> {
        self.items.iter().cloned().collect()
    }
}

Container<i32>::sum is available; Container<String>::sum is not. Container<T>::duplicate is available for any cloneable T.

Associated types

Some traits use associated types — types determined by the implementation rather than by a separate generic parameter:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> { /* ... */ }
}

The type Item = u32 admits one canonical type per implementation; Counter::Item is u32.

The contrast with generic parameters:

// Generic parameter:
trait Container<T> { /* ... */ }
impl Container<i32> for IntVec { /* ... */ }
impl Container<f64> for IntVec { /* ... */ }     // could implement multiple

// Associated type:
trait Container { type Item; /* ... */ }
impl Container for IntVec {
    type Item = i32;                               // only one
}

The associated-type form admits one canonical type per implementation; the generic-parameter form admits multiple.

The conventional choice:

  • Associated types when each type has a single natural choice (Iterator::Item, Add::Output).
  • Generic parameters when a type may legitimately implement the trait multiple ways.

Const generics

const generics admit value parameters — typically integer constants:

struct Array<T, const N: usize> {
    items: [T; N],
}

impl<T: Default + Copy, const N: usize> Array<T, N> {
    fn new() -> Self {
        Array { items: [T::default(); N] }
    }
}

let a: Array<i32, 5> = Array::new();
let b: Array<f64, 100> = Array::new();

The const N: usize admits the array size as a compile-time parameter. The mechanism is the conventional Rust form for fixed-size arrays.

The standard library uses const generics extensively for arrays:

impl<T, const N: usize> [T; N] {
    fn len(&self) -> usize { N }
}

let a: [i32; 5] = [0, 1, 2, 3, 4];
let n = a.len();                                  // 5

Const generics admit specifying the size as part of the type — [i32; 5] and [i32; 6] are different types. The mechanism admits compile-time guarantees about sizes that runtime-dimensional Vec<T> cannot.

Generic constraints with where

For complex constraints:

fn first_or_default<T, I>(iter: I) -> T
where
    T: Default,
    I: IntoIterator<Item = T>,
{
    iter.into_iter().next().unwrap_or_else(T::default)
}

The where admits multiple constraints with their own type relationships.

For higher-order constraints:

fn for_each<T, F>(items: &[T], mut f: F)
where
    F: FnMut(&T),
{
    for item in items {
        f(item);
    }
}

The F: FnMut(&T) admits a closure that modifies its environment and takes a &T. Treated in Functions and closures.

Bounded generics

A generic parameter may be bounded with a lifetime and other constraints:

fn longest<'a, T>(list: &'a [T]) -> &'a T
where
    T: PartialOrd,
{
    // ...
}

The function takes a lifetime parameter 'a and a type parameter T, with T: PartialOrd as the bound.

The combination of lifetimes and types is conventional in Rust; many functions have both.

Monomorphisation

Rust generics are monomorphised — each instantiation produces specialised code:

fn double<T: Add<Output = T> + Copy>(x: T) -> T {
    x + x
}

let a = double(5_i32);                            // produces double::<i32>
let b = double(3.14_f64);                          // produces double::<f64>

The compiler generates two distinct functions, one per type, each with the type-specific implementation. The cost: larger binaries (each instantiation duplicates code). The benefit: zero runtime overhead — generic code performs identically to hand-specialised code.

For dynamic dispatch (avoiding code duplication at the cost of runtime overhead), use trait objects:

fn print(item: &dyn Display) {                    // dynamic dispatch; no monomorphisation
    println!("{}", item);
}

Treated in Traits.

Generic methods

Methods may take their own type parameters separate from the type’s:

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

impl<T> Container<T> {
    fn map<U, F>(&self, f: F) -> Container<U>
    where
        F: Fn(&T) -> U,
    {
        Container {
            items: self.items.iter().map(f).collect(),
        }
    }
}

The method map introduces type parameter U and F; the type’s T and the method’s U/F are independent.

Default type parameters

A type parameter may have a default:

struct Container<T = i32> {
    items: Vec<T>,
}

let c: Container = Container { items: vec![1, 2, 3] };  // uses T = i32
let s: Container<String> = Container { items: vec![] };  // T = String

Defaults are conventional for parameters that are usually a particular type but admit alternatives. The standard library’s Hashmap<K, V, S = RandomState> uses a default for the hasher.

Generic associated types (GATs)

Generic associated types admit type parameters on associated types:

trait Streaming {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

The Item<'a> is a generic associated type — it admits a different lifetime parameter per call. GATs (stabilised in 1.65) admit substantial advanced patterns; they are rare in routine code but useful in libraries that need them.

Common patterns

Generic data structure

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 peek(&self) -> Option<&T> {
        self.items.last()
    }
}

The Stack<T> admits any element type without sacrificing the type-safety of the contained elements.

Generic algorithm

fn first_match<T, F>(items: &[T], pred: F) -> Option<&T>
where
    F: Fn(&T) -> bool,
{
    for item in items {
        if pred(item) {
            return Some(item);
        }
    }
    None
}

let nums = vec![1, 2, 3, 4, 5];
let result = first_match(&nums, |&n| n > 3);

The function admits any element type and any predicate.

Generic constraint propagation

struct Wrapper<T>(T);

impl<T: Clone> Clone for Wrapper<T> {
    fn clone(&self) -> Self {
        Wrapper(self.0.clone())
    }
}

The impl<T: Clone> admits Wrapper<T> to be cloneable when its inner T is cloneable.

Default implementation

trait Greeter {
    fn name(&self) -> String;

    fn greet(&self) -> String {
        format!("Hello, {}", self.name())
    }
}

The greet has a default; implementers need only provide name.

Generic builder

struct Builder<T> {
    state: Option<T>,
}

impl<T: Default> Builder<T> {
    fn new() -> Self {
        Builder { state: Some(T::default()) }
    }

    fn build(self) -> T {
        self.state.expect("not initialised")
    }
}

impl<T: Default + Display> Builder<T> {
    fn display(&self) -> String {
        format!("{}", self.state.as_ref().unwrap())
    }
}

Different impl blocks admit different methods based on the bounds.

Const generic for fixed-size buffer

struct CircularBuffer<T, const N: usize> {
    items: [Option<T>; N],
    pos: usize,
}

impl<T, const N: usize> CircularBuffer<T, N> {
    fn new() -> Self {
        Self {
            items: [const { None }; N],            // const-context expression (1.79+)
            pos: 0,
        }
    }
}

let buf: CircularBuffer<i32, 100> = CircularBuffer::new();

The const N: usize admits compile-time-known buffer sizes.

Phantom types for type-level distinctions

use std::marker::PhantomData;

struct UserId<T> {
    id: u32,
    _marker: PhantomData<T>,
}

struct Admin;
struct Regular;

let admin: UserId<Admin> = UserId { id: 1, _marker: PhantomData };
let user: UserId<Regular> = UserId { id: 2, _marker: PhantomData };

fn admin_only(id: UserId<Admin>) {
    // ...
}

admin_only(admin);                                 // OK
// admin_only(user);                                  // type error

The PhantomData<T> admits a generic parameter that doesn’t appear in any field. The mechanism admits type-level distinctions for compile-time safety without runtime overhead.

Variance

Generic types have variance — how their generics relate to subtyping. Rust’s lifetimes and references have variance rules that determine when a Foo<&'a T> may be used where Foo<&'b T> is expected.

For most code, variance is invisible; the compiler determines it automatically. For libraries with elaborate generic types (typically lifetimes), variance may need explicit consideration through PhantomData markers.

A note on the conventional discipline

The contemporary Rust generic advice:

  • Use generics for code reuse — they are zero-cost.
  • Use trait bounds to constrain to admissible types.
  • Use where clauses for non-trivial bounds.
  • Use associated types when each implementation has one canonical type.
  • Use impl Trait for return positions and (sugar) for argument positions.
  • Use trait objects when generics produce too much code or runtime polymorphism is needed.
  • Use const generics for fixed-size arrays and similar.

The combination — generic types and functions, trait bounds, associated types, const generics — admits substantial code reuse with compile-time guarantees and zero-cost abstractions. The discipline of writing well-typed Rust generics is one of the principal skills of the language.