11.2 Generics in Rust

11.2.1 What Are Generics?

Generics allow you to write code that can operate on different types without sacrificing type safety. They enable parameterization of types and functions, making your code more flexible and reusable.

Key Points:

  • Type Parameters: Generics use type parameters to represent types in a generic way.
  • Syntax: Type parameters are specified within angle brackets <> after the name of the function, struct, enum, or method.
  • Type Safety: Rust ensures that generics are used safely at compile time.
  • Code Reuse: Generics prevent code duplication by allowing the same code to work with different types.

Typically, capital letters like T, U, or V are used as type parameter names for generics.

11.2.2 Generic Functions

You can define functions that are generic over one or more types.

Syntax:

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

Here, T is a generic type parameter.

Example: Generic max Function

First, let's consider two functions that find the maximum of two numbers, one for i32 and one for f64.

#![allow(unused)]
fn main() {
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 }
}
}

These functions are nearly identical. Using generics, we can write a single max function that works for any type that can be ordered.

#![allow(unused)]
fn main() {
fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
}
  • Trait Bound: T: PartialOrd ensures that T implements the PartialOrd trait, which provides the > operator.

Using the Generic max Function:

fn main() {
    let int_max = max(10, 20);
    let float_max = max(1.5, 3.7);
    println!("int_max: {}, float_max: {}", int_max, float_max);
}

Another Example: Generic size_of_val Function

The size_of_val function can be another example that works without explicit trait bounds.

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));
}

11.2.3 Generic Structs and Enums

You can define structs and enums with generic type parameters.

Generic Struct with Different Types:

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

fn main() {
    let pair = Pair { first: 5, second: 3.14 };
    println!("Pair: ({}, {})", pair.first, pair.second);
}
  • Here, Pair is a struct with two fields of potentially different types, T and U.

Generic Data Structures:

Rust's standard library provides several generic data structures, such as:

  • Vectors: Vec<T> - A growable array type.

    #![allow(unused)]
    fn main() {
    let mut numbers: Vec<i32> = Vec::new();
    numbers.push(1);
    numbers.push(2);
    println!("{:?}", numbers);
    }
  • Hash Maps: HashMap<K, V> - A hash map type.

    #![allow(unused)]
    fn main() {
    use std::collections::HashMap;
    
    let mut scores: HashMap<String, i32> = HashMap::new();
    scores.insert(String::from("Alice"), 10);
    scores.insert(String::from("Bob"), 20);
    println!("{:?}", scores);
    }

11.2.4 Generic Methods

Methods can also be generic over types.

Example:

impl<T, U> Pair<T, U> {
    fn swap(self) -> Pair<U, T> {
        Pair {
            first: self.second,
            second: self.first,
        }
    }
}
  • The swap method swaps the first and second fields of the Pair.

11.2.5 Trait Bounds in Generics

When using generics, you often need to specify constraints on the types, known as trait bounds. This ensures that the types used with your generic code implement the traits required for the operations you perform.

Example:

use std::fmt::Display;

fn print_pair<T: Display, U: Display>(pair: &Pair<T, U>) {
    println!("Pair: ({}, {})", pair.first, pair.second);
}
  • The trait bounds T: Display and U: Display ensure that first and second can be formatted using {}.

11.2.6 Specifying Multiple Trait Bounds with the + Syntax

You can specify multiple trait bounds for a generic type using the + syntax.

Example:

#![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);
    }
}
}
  • Here, T must implement both PartialOrd and Display.

11.2.7 Using where Clauses for Cleaner Syntax

For complex trait bounds, you can use where clauses to improve readability.

Example:

#![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.8 Generics and Code Bloat

While generics provide flexibility, they can lead to code bloat if overused with many different types, especially if the generic functions are large.

  • Monomorphization: Rust generates specialized versions of generic functions for each concrete type used.
  • Trade-off: While this ensures zero-cost abstractions, excessive use with many types can increase the compiled binary size.

Note: It's important to balance the flexibility of generics with the potential impact on binary size.

11.2.9 Comparing Rust Generics to C++ Templates

While Rust's generics may seem similar to C++ templates, there are significant differences:

  • Type Safety and Monomorphization: Rust's generics are monomorphized at compile time, similar to C++ templates, but with stricter type checking, leading to safer code.

    Monomorphization is the process by which the compiler generates concrete implementations of generic functions and types for each specific set of type arguments used in the code. This means that generic code is compiled into specialized versions for each type, resulting in optimized and type-safe code.

  • No Specialization: Rust does not currently support template specialization like C++.

  • Constraints: Rust requires you to specify trait bounds explicitly, whereas C++ allows more implicit usage.

  • Associated Types and Lifetimes: Rust's generics work closely with traits, lifetimes, and associated types to provide powerful abstractions.

Key Takeaway: Rust's generics provide the flexibility of C++ templates but with additional safety guarantees and integration with traits and lifetimes.