Introduction to Generic Programming
Generic programming is a style of writing functions, data structures, and traits that operate on abstract type placeholders rather than single concrete types. In Rust, those placeholders are called type parameters, written inside angle brackets (<T>) and replaced with concrete types such as i32, f64, or String when the code is compiled.
Eliminating Type-Specific Duplication
Without generics, performing the same structural operation across multiple types requires duplicating the function or data structure for each concrete type.
fn first_i32(slice: &[i32]) -> Option<&i32> {
if slice.is_empty() {
None
} else {
Some(&slice[0])
}
}
fn first_str<'a>(slice: &'a [&'a str]) -> Option<&'a &'a str> {
if slice.is_empty() {
None
} else {
Some(&slice[0])
}
}
fn main() {
let numbers = [10, 20, 30];
let words = ["rust", "cargo"];
println!("{:?}", first_i32(&numbers));
println!("{:?}", first_str(&words));
}
Some(10)
Some("rust")
Replacing the concrete element type with a type parameter T consolidates both functions into a single generic definition while preserving full static type checking:
fn first<T>(slice: &[T]) -> Option<&T> {
if slice.is_empty() {
None
} else {
Some(&slice[0])
}
}
fn main() {
let numbers = [10, 20, 30];
let words = ["rust", "cargo"];
println!("{:?}", first(&numbers));
println!("{:?}", first(&words));
}
Some(10)
Some("rust")
Where Generics Appear in Rust
Rust supports generic parameters across five primary language constructs:
| Construct | Syntax Example | Purpose |
|---|---|---|
| Function | fn swap<T>(a: &mut T, b: &mut T) |
Algorithm parameterized over input or output types |
| Struct | struct Pair<T> { left: T, right: T } |
Record whose fields hold values of type T |
| Enum | enum Option<T> { Some(T), None } |
Tagged union whose variants carry payloads of type T |
| Implementation block | impl<T> Pair<T> { ... } |
Methods or trait implementations defined for all T |
| Trait | trait From<T> { fn from(value: T) -> Self; } |
Interface parameterized over an external type T |
In addition to type parameters (T), angle brackets can also declare lifetime parameters ('a) and const generic parameters (const N: usize):
#[derive(Debug)]
struct FixedBuffer<'a, T, const N: usize> {
label: &'a str,
items: [T; N],
}
fn main() {
let buf = FixedBuffer {
label: "rgb",
items: [255_u8, 128, 0],
};
println!("{}: {:?}", buf.label, buf.items);
}
rgb: [255, 128, 0]
Early Type Checking of Generic Definitions
Unlike C++ templates, which defer type checking until a template is instantiated with a concrete type, Rust type-checks a generic function or struct body against its declared trait bounds at definition time. A generic body can only perform operations that are guaranteed to be valid for every possible type satisfying its bounds.
// ❌ does not compile
fn print_value<T>(value: T) {
println!("{value}");
}
fn main() {
print_value(42);
}
error[E0277]: `T` doesn't implement `std::fmt::Display`
--> src/main.rs:2:15
|
2 | println!("{value}");
| ^^^^^^^ `T` cannot be formatted with the default formatter
|
help: consider restricting type parameter `T` with trait `Display`
|
1 | fn print_value<T: std::fmt::Display>(value: T) {
| +++++++++++++++++++
Adding the required std::fmt::Display bound makes the contract explicit in the signature and allows the body to compile:
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("{value}");
}
fn main() {
print_value(42);
print_value("generic string");
}
42
generic string
Zero-Cost Abstraction Model
Rust compiles generic code through monomorphization: at compile time, the compiler generates a specialized machine-code version of each generic item for every concrete type combination used in the program.
Generic Source Compiler Monomorphization (LLVM IR)
+------------------------+ +-----------------------------------+
| fn id<T>(x: T) -> T | --> | fn id_i32(x: i32) -> i32 |
| | --> | fn id_f64(x: f64) -> f64 |
+------------------------+ +-----------------------------------+
Because specialization happens before code generation, generic functions and structs incur zero runtime type-checking overhead, zero boxing overhead, and zero virtual dispatch cost compared to hand-written concrete equivalents.