8.8 Generic Functions

Generics allow writing functions that can operate on values of multiple different types, while still maintaining type safety. This avoids source code duplication. Generic functions declare type parameters (typically denoted by T, U, etc.) enclosed in angle brackets (<>) after the function name. These type parameters then act as placeholders for concrete types within the function’s signature (for parameters and return types) and body. Often, these type parameters require specific capabilities, expressed using trait bounds.

Generics are a large topic, covered more extensively in Chapter 11, but here’s an introduction.

Example: A Generic max function

Without generics, you’d need separate functions for i32, f64, etc.

fn max_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}
fn max_f64(a: f64, b: f64) -> f64 {
    if a > b { a } else { b }
}
// ... potentially more versions

With generics, you write one function:

use std::cmp::PartialOrd; // Trait required for comparison operators like >

// T is a type parameter.
// T: PartialOrd is a trait bound, meaning T must implement PartialOrd.
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    println!("Max of 5 and 10: {}", max_generic(5, 10));        // Works with i32
    println!("Max of 3.14 and 2.71: {}", max_generic(3.14, 2.71)); // Works with f64
    println!("Max of 'a' and 'z': {}", max_generic('a', 'z'));   // Works with char
}
  • <T: PartialOrd>: Declares a generic type T that must implement the PartialOrd trait (which provides comparison methods like > and <).
  • The function signature uses T wherever a concrete type (like i32) would have been used.

The compiler generates specialized versions of the generic function for each concrete type used at compile time (e.g., one version for i32, one for f64). This process is called monomorphization, ensuring generic code runs just as efficiently as specialized code, without runtime overhead.