11.2 Generics in Rust

Generics let you write code that can handle various data types without sacrificing compile-time safety. They help you avoid code duplication by parameterizing functions, structs, enums, and methods over abstract type parameters.

Key Points

  • Type Parameters: Expressed using angle brackets (<>), often named T, U, V, etc.
  • Zero-Cost Abstractions: Rust enforces type checks at compile time, and generics compile to specialized, efficient machine code.
  • Flexibility: The same generic definition can accommodate multiple concrete types.
  • Contrast with C: In C, a similar effect might be achieved via macros or void pointers, but neither approach provides the robust type checking Rust offers.

11.2.1 Generic Functions

Functions can accept or return generic types:

fn function_name<T>(param: T) {
    // ...
}

Example: A Generic max Function

Instead of writing nearly identical functions for i32 and f64, we can unify them:

#![allow(unused)]
fn main() {
fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
}

T: PartialOrd specifies that T must support comparisons.

Example: A Generic size_of_val Function

use std::mem;

fn size_of_val<T>(_: &T) -> usize {
    mem::size_of::<T>()
}

fn main() {
    let x = 5;
    let y = 3.14;
    println!("Size of x: {}", size_of_val(&x));
    println!("Size of y: {}", size_of_val(&y));
}

This function determines the size of any type you pass in. Because mem::size_of works for all types, we do not require a specific trait bound here.

11.2.2 Generic Structs and Enums

You can define structs and enums with generics:

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

fn main() {
    let pair = Pair { first: 5, second: 3.14 };
    println!("Pair: ({}, {})", pair.first, pair.second);
}

Examples in the Standard Library:

  • Vec<T>: A dynamic growable list whose elements are of type T.
  • HashMap<K, V>: A map of keys K to values V.

11.2.3 Generic Methods

Generic parameters apply to methods as well:

impl<T, U> Pair<T, U> {
    fn swap(self) -> Pair<U, T> {
        Pair {
            first: self.second,
            second: self.first,
        }
    }
}

11.2.4 Trait Bounds in Generics

It’s common to require that generic parameters implement certain traits:

use std::fmt::Display;

fn print_pair<T: Display, U: Display>(pair: &Pair<T, U>) {
    println!("Pair: ({}, {})", pair.first, pair.second);
}

11.2.5 Multiple Trait Bounds Using +

You can require multiple traits on a single parameter:

#![allow(unused)]
fn main() {
fn compare_and_display<T: PartialOrd + Display>(a: T, b: T) {
    if a > b {
        println!("{} is greater than {}", a, b);
    } else {
        println!("{} is less than or equal to {}", a, b);
    }
}
}

11.2.6 Using where Clauses for Clarity

When constraints are numerous or lengthy, where clauses help readability:

#![allow(unused)]
fn main() {
fn compare_and_display<T, U>(a: T, b: U)
where
    T: PartialOrd<U> + Display,
    U: Display,
{
    if a > b {
        println!("{} is greater than {}", a, b);
    } else {
        println!("{} is less than or equal to {}", a, b);
    }
}
}

11.2.7 Generics and Code Bloat

Because Rust monomorphizes generic code (creating specialized versions for each concrete type), your binary may grow when you heavily instantiate generics:

  • Trade-Off: In exchange for potential code-size increases, you gain compile-time safety and optimized code for each specialized version.

11.2.8 Comparing Rust Generics to C++ Templates

Rust generics resemble C++ templates in that both are expanded at compile time. However, Rust’s approach is more stringent in terms of type checking:

  • Stricter Bounds: Rust ensures all required traits are satisfied at compile time, reducing surprises.
  • No Specialization: Rust does not currently support template specialization, although associated traits and types often achieve similar outcomes.
  • Seamless Integration with Lifetimes: Rust extends type parameters to encompass lifetime parameters, providing memory safety features.
  • Zero-Cost Abstraction: Monomorphization yields efficient code akin to specialized C++ templates.