Const Generics
Const generics allow types, traits, and functions to be parameterized by compile-time constant values (const N: usize, const FLAG: bool, const TAG: char) alongside lifetime ('a) and type (T) parameters. With const generics, values like array lengths, matrix dimensions, ring-buffer capacities, and protocol version numbers become part of the static type signature.
The Three Kinds of Generic Parameters in Rust
| Parameter Kind | Syntax in <...> |
Example Instantiation | Checked At |
|---|---|---|---|
| Lifetime parameter | 'a, 'src |
View<'static> |
Borrow checking |
| Type parameter | T, E: Error |
Vec<u32> |
Type checking & monomorphization |
| Const parameter | const N: usize, const B: bool |
Matrix<f64, 4, 4> |
Const evaluation & type checking |
When a type or function combines all three, lifetime parameters must come first, while type and const parameters can be interleaved in any logical order (commonly types before consts: <'a, T, const N: usize>).
Standard Library Array Trait Implementations via Const Generics
Before const generics were stabilized in Rust 1.51, standard traits like Clone, Debug, PartialEq, IntoIterator, and Default had to be manually implemented for arrays [T; 0] through [T; 32]. With const N: usize, a single generic implementation covers arrays of any length N:
// Conceptual standard library implementation for arrays of ANY length N:
// impl<T, const N: usize> IntoIterator for [T; N] { ... }
fn sum_fixed_array<const N: usize>(arr: [i64; N]) -> i64 {
let mut sum = 0;
// Direct by-value iteration over `[i64; N]` for any `N`:
for item in arr {
sum += item;
}
sum
}
fn main() {
let small: [i64; 3] = [10, 20, 30];
let large: [i64; 65] = [2; 65];
println!("sum of [i64; 3]: {}", sum_fixed_array(small));
println!("sum of [i64; 65]: {}", sum_fixed_array(large));
}
sum of [i64; 3]: 60
sum of [i64; 65]: 130
Type-Checked Matrix Multiplication (Matrix<R, C> * Matrix<C, K> = Matrix<R, K>)
By encoding row and column counts as const generic parameters, incompatible matrix dimensions (such as multiplying a $2 \times 3$ matrix by a $4 \times 2$ matrix) are caught at compile time with zero runtime bounds checks:
#[derive(Debug, PartialEq)]
struct Matrix<const ROWS: usize, const COLS: usize> {
data: [[i32; COLS]; ROWS],
}
impl<const ROWS: usize, const COMMON: usize> Matrix<ROWS, COMMON> {
fn mul<const COLS: usize>(&self, rhs: &Matrix<COMMON, COLS>) -> Matrix<ROWS, COLS> {
let mut out = Matrix {
data: [[0; COLS]; ROWS],
};
for r in 0..ROWS {
for c in 0..COLS {
let mut dot = 0;
for k in 0..COMMON {
dot += self.data[r][k] * rhs.data[k][c];
}
out.data[r][c] = dot;
}
}
out
}
}
fn main() {
let a: Matrix<2, 3> = Matrix {
data: [[1, 2, 3], [4, 5, 6]],
};
let b: Matrix<3, 2> = Matrix {
data: [[7, 8], [9, 1], [2, 3]],
};
// `Matrix<2, 3>` * `Matrix<3, 2>` produces `Matrix<2, 2>`:
let product: Matrix<2, 2> = a.mul(&b);
println!("product: {:?}", product.data);
}
product: [[31, 19], [85, 55]]
Monomorphization Trade-Offs with Const Generics
Just like generic type parameters, each distinct constant value (sum_fixed_array::<3>, sum_fixed_array::<65>) generates a separate monomorphized machine-code copy of the function. When the constant is only needed as a runtime loop bound and does not change the memory layout of a struct or return array, passing slice: &[T] instead of arr: &[T; N] avoids duplicating machine code per length N.