On this page

Generics: zero-cost abstractions

14 min read TextCh. 4 — Abstractions

Generics: writing reusable code

Generics allow you to write functions, structs, and enums that work with multiple types without duplicating code. In Rust, generics are a core language feature used throughout the standard library: Vec<T>, Option<T>, Result<T, E>, HashMap<K, V>.

Generic functions

The problem without generics: you need one function per type.

// Without generics: duplicated code
fn maximum_i32(list: &[i32]) -> i32 {
    let mut max = list[0];
    for &n in list {
        if n > max { max = n; }
    }
    max
}

fn maximum_f64(list: &[f64]) -> f64 {
    let mut max = list[0];
    for &n in list {
        if n > max { max = n; }
    }
    max
}

With generics, a single function handles any comparable type:

// T is the generic type parameter
// PartialOrd: T must support > and < comparisons
// Copy: T must be copyable (to assign max = list[0])
fn maximum<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &item in list.iter() {
        if item > max {
            max = item;
        }
    }
    max
}

fn main() {
    let integers = vec![34, 50, 25, 100, 65];
    println!("Maximum integer: {}", maximum(&integers));
    
    let floats = [2.5, 1.7, 9.3, 4.1, 7.8];
    println!("Maximum float: {}", maximum(&floats));
    
    let chars = ['y', 'm', 'a', 'q'];
    println!("Maximum char: {}", maximum(&chars));
}

Generic structs

#[derive(Debug, Clone)]
struct Stack<T> {
    elements: Vec<T>,
    max_capacity: usize,
}

impl<T> Stack<T> {
    fn new(capacity: usize) -> Self {
        Stack {
            elements: Vec::with_capacity(capacity),
            max_capacity: capacity,
        }
    }
    
    fn push(&mut self, value: T) -> Result<(), &'static str> {
        if self.elements.len() >= self.max_capacity {
            return Err("Stack is full");
        }
        self.elements.push(value);
        Ok(())
    }
    
    fn pop(&mut self) -> Option<T> {
        self.elements.pop()
    }
    
    fn peek(&self) -> Option<&T> {
        self.elements.last()
    }
    
    fn is_empty(&self) -> bool {
        self.elements.is_empty()
    }
    
    fn len(&self) -> usize {
        self.elements.len()
    }
}

// Implementation specific to types implementing Display
impl<T: std::fmt::Display> Stack<T> {
    fn display(&self) {
        print!("[Top] ");
        for el in self.elements.iter().rev() {
            print!("{el} ");
        }
        println!("[Bottom]");
    }
}

fn main() {
    let mut num_stack: Stack<i32> = Stack::new(5);
    num_stack.push(1).unwrap();
    num_stack.push(2).unwrap();
    num_stack.push(3).unwrap();
    num_stack.display();
    
    println!("Pop: {:?}", num_stack.pop());
    println!("Peek: {:?}", num_stack.peek());
    
    let mut text_stack: Stack<&str> = Stack::new(3);
    text_stack.push("hello").unwrap();
    text_stack.push("world").unwrap();
    text_stack.display();
}

Generic enums

You already know the most important generic enums from the std:

// As defined in the standard library:
// enum Option<T> { Some(T), None }
// enum Result<T, E> { Ok(T), Err(E) }

// You can also create generic enums:
#[derive(Debug)]
enum Tree<T> {
    Leaf(T),
    Node {
        value: T,
        left: Box<Tree<T>>,
        right: Box<Tree<T>>,
    },
}

impl<T: std::fmt::Display + PartialOrd> Tree<T> {
    fn contains(&self, target: &T) -> bool {
        match self {
            Tree::Leaf(v) => v == target,
            Tree::Node { value, left, right } => {
                value == target
                || left.contains(target)
                || right.contains(target)
            }
        }
    }
}

fn main() {
    let tree = Tree::Node {
        value: 10,
        left: Box::new(Tree::Node {
            value: 5,
            left: Box::new(Tree::Leaf(3)),
            right: Box::new(Tree::Leaf(7)),
        }),
        right: Box::new(Tree::Leaf(15)),
    };
    
    println!("Contains 7? {}", tree.contains(&7));
    println!("Contains 6? {}", tree.contains(&6));
}

Multiple bounds with where

When bounds get complex, the where clause improves readability:

use std::fmt::{Debug, Display};
use std::ops::Add;

// Without where (hard to read):
fn process<T: Display + Debug + Clone + PartialOrd>(value: T) -> T { value }

// With where (much clearer):
fn serialize<T>(collection: &[T]) -> String
where
    T: Display + Debug + Clone,
{
    collection.iter()
        .map(|item| format!("{item}"))
        .collect::<Vec<_>>()
        .join(", ")
}

// Multiple generic parameters with different bounds
fn blend<A, B, C>(a: A, b: B) -> C
where
    A: Into<C>,
    B: Into<C>,
    C: Add<Output = C>,
{
    a.into() + b.into()
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    println!("{}", serialize(&numbers));
    
    let words = ["hello", "world", "rust"];
    println!("{}", serialize(&words));
    
    // blend i32 + i32 → i64
    let result: i64 = blend(10_i32, 20_i32);
    println!("Result: {result}");
}

Monomorphization: why generics are free

When the compiler compiles generic code, it generates a specialized version for each concrete type used. This process is called monomorphization:

fn identity<T>(value: T) -> T { value }

fn main() {
    let n = identity(42_i32);       // Compiler generates identity_i32
    let s = identity("hello");      // Compiler generates identity_str
    let f = identity(3.14_f64);     // Compiler generates identity_f64
    
    println!("{n} {s} {f}");
}

The resulting binary contains optimized code specific to each type — exactly as if you had written three separate functions. No vtable overhead, no boxing, no runtime costs.

This contrasts with languages like Java/C# where generics use boxing and can have overhead, or older Go versions where generics were not available.

Turbofish: explicitly specifying types

Sometimes the compiler cannot infer the generic type. Use the "turbofish" syntax ::<T>:

fn main() {
    // Without turbofish — compiler needs context
    let numbers: Vec<i32> = Vec::new();
    
    // With turbofish on the method
    let v = Vec::<i32>::new();
    
    // In generic function calls
    let n = "42".parse::<i32>().unwrap();
    let f = "3.14".parse::<f64>().unwrap();
    
    // In collect
    let squares = (1..=5).map(|n| n * n).collect::<Vec<_>>();
    println!("{:?}", squares);
    
    // In from
    let s = String::from("hello");
    let v2 = Vec::<u8>::from(s.as_bytes());
    println!("{:?}", v2);
}

Generics in impl blocks: conditional implementations

You can implement methods only for certain generic types:

use std::fmt::Display;

struct Wrapped<T> {
    value: T,
}

impl<T> Wrapped<T> {
    fn new(value: T) -> Self {
        Wrapped { value }
    }
    
    fn get(&self) -> &T {
        &self.value
    }
}

// Only available when T implements Display
impl<T: Display> Wrapped<T> {
    fn show(&self) {
        println!("Wrapped: {}", self.value);
    }
}

// Only available when T implements Clone
impl<T: Clone> Wrapped<T> {
    fn duplicate(&self) -> (T, T) {
        (self.value.clone(), self.value.clone())
    }
}

fn main() {
    let w = Wrapped::new(42);
    w.show();                              // available: i32 impl Display
    let (a, b) = w.duplicate();           // available: i32 impl Clone
    println!("Duplicated: {a}, {b}");
    
    let w2 = Wrapped::new(vec![1, 2, 3]);
    // w2.show(); // Vec<i32> implements Debug but not Display by default
    let (v1, v2) = w2.duplicate();        // available: Vec impl Clone
    println!("{:?} {:?}", v1, v2);
}

With a solid understanding of generics and traits, you are ready for the error handling lesson — where Result<T, E> and the ? operator shine at full power.

Monomorphization: zero overhead
Generics in Rust have zero runtime cost. The compiler generates a specialized version of the function for each concrete type you use — this is called monomorphization. maximum::<i32> and maximum::<f64> are completely different functions in the binary, each optimized for its type.
Turbofish ::<T> for disambiguation
When the compiler cannot infer the generic type, use turbofish syntax: let v = "42".parse::<i32>().unwrap(); or vec![].into_iter().collect::<Vec<i32>>(). The angle brackets go after the function/method name.
rust
// Generic function: T must implement PartialOrd and Copy
fn maximum<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &item in list.iter() {
        if item > max {
            max = item;
        }
    }
    max
}

// Generic struct
#[derive(Debug)]
struct Pair<T, U> {
    first: T,
    second: U,
}

impl<T: std::fmt::Display, U: std::fmt::Display> Pair<T, U> {
    fn new(first: T, second: U) -> Self {
        Pair { first, second }
    }

    fn show(&self) {
        println!("({}, {})", self.first, self.second);
    }
}

// Implementation only for pairs of the same type
impl<T: std::fmt::Display + PartialOrd> Pair<T, T> {
    fn larger(&self) -> &T {
        if self.first > self.second { &self.first } else { &self.second }
    }
}

fn main() {
    let integers = vec![34, 50, 25, 100, 65];
    println!("Maximum: {}", maximum(&integers));

    let floats = [2.5, 1.7, 9.3, 4.1];
    println!("Maximum: {}", maximum(&floats));

    let p1 = Pair::new("hello", 42);
    p1.show();

    let p2 = Pair::new(10_i32, 20_i32);
    println!("Larger: {}", p2.larger());
}