When size matters, bake it into the type
You're building a graphics library. You have a rotation matrix that's always 3x3 and a transformation matrix that's always 4x4. You write a function to multiply matrices. You pass a 3x3 and a 4x4. In Python, the code runs until it crashes with an index error. In Rust, you want the compiler to reject the call before you even run the program.
Const generics make the size part of the type. [f64; 3] and [f64; 4] are completely different types. The compiler treats the length as a type parameter. You can write functions that work for any size, or constrain the size so operations are only valid when dimensions match. The size is a contract enforced at compile time.
Const values as type parameters
Generics usually stand for types. Vec<T> works for Vec<i32> or Vec<String>. The T is a type placeholder. Const generics add a parameter that's a value, not a type. [T; N] means "an array of type T with length N". The length N is part of the type signature.
Think of a cookie cutter. A [i32; 3] is a cookie cut by a 3-slot cutter. A [i32; 4] is a cookie from a 4-slot cutter. They look similar, but they don't fit in the same box. The shape is determined by the cutter, and the cutter size is baked into the cookie's identity.
The syntax uses const in the generic list. const N: usize declares a const generic parameter named N with type usize. The compiler requires N to be known at compile time. You can use N anywhere a constant expression is allowed.
Convention aside: N is the standard name for a size parameter. CAP or CAPACITY is common for buffer limits. The type is almost always usize because array lengths are indices. You can use other primitive types, but usize is the convention.
Minimal example
Here is a function that sums an array of any size. The size is a const generic parameter.
/// Sums an array of any size. N is a const generic parameter.
fn sum_array<const N: usize>(arr: [i32; N]) -> i32 {
// N is a compile-time constant.
// The compiler uses N to check bounds and optimize loops.
arr.iter().sum()
}
fn main() {
// [i32; 3] and [i32; 4] are distinct types.
let small = [1, 2, 3];
let big = [1, 2, 3, 4];
// The compiler generates separate versions for each size.
// sum_array<i32, 3> and sum_array<i32, 4>.
println!("{}", sum_array(small));
println!("{}", sum_array(big));
}
The size isn't just data. It's part of the type's DNA.
What happens under the hood
When you call sum_array with a [i32; 3], the compiler generates a version of the function where N is replaced by 3. It generates another version for 4. This is monomorphization. The compiler creates a separate copy of the function for each distinct set of generic parameters.
There is no runtime cost. The size is baked into the code. The compiler knows the exact length. It can unroll loops. It can allocate the array on the stack. It can prove bounds checks are unnecessary. If you access arr[0], the compiler knows N is at least 1 if the call site provides a non-empty array.
The performance is identical to hand-written code for a specific size. Const generics are a zero-cost abstraction. You get the flexibility of generics with the speed of hardcoded sizes.
Convention aside: The community calls this "zero-cost" because the abstraction disappears at runtime. The generated code is what you would write if you manually duplicated the function for each size.
Realistic example: type-safe matrices
Const generics shine when dimensions must match. A matrix multiplication requires the inner dimensions to be equal. You can encode this rule in the type system.
/// A fixed-size matrix. Rows and columns are const generics.
struct Matrix<const R: usize, const C: usize> {
data: [[f64; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
/// Multiplies two matrices. Inner dimensions must match.
fn multiply<const M: usize>(self, other: Matrix<C, M>) -> Matrix<R, M> {
// The type system enforces that self.C == other.R.
// If you pass a Matrix<C, M> where M != self.C, it won't compile.
// The compiler checks the const generic parameters.
// Initialize result matrix with zeros.
let mut result = Matrix { data: [[0.0; M]; R] };
// Perform multiplication.
// The loops are bounded by const generics.
// The compiler can optimize these heavily.
for i in 0..R {
for j in 0..M {
for k in 0..C {
result.data[i][j] += self.data[i][k] * other.data[k][j];
}
}
}
result
}
}
fn main() {
// 2x3 matrix.
let a = Matrix {
data: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
};
// 3x2 matrix.
let b = Matrix {
data: [[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]],
};
// Result is 2x2.
let c = a.multiply(b);
// This would fail to compile:
// let d = Matrix { data: [[1.0, 2.0], [3.0, 4.0]] };
// a.multiply(d); // Error: expected Matrix<3, M>, found Matrix<2, 2>
}
The multiply function takes other: Matrix<C, M>. The C comes from self. The compiler ensures the second matrix has C rows. If you pass a matrix with the wrong number of rows, you get a type error. The dimensions are checked at compile time.
Trust the type system. If the dimensions match, the code compiles. If they don't, the compiler stops you.
Pitfalls and compiler errors
Const generics are powerful, but they have limits.
Code bloat. Monomorphization generates a copy of the code for each size. If you use Buffer<1>, Buffer<2>, ..., Buffer<1000>, you get 1000 versions of the code. This increases binary size. Use const generics for sizes that are reused. If you have thousands of distinct sizes, the bloat can be significant.
Zero-size arrays. The compiler doesn't assume N is positive. If you index arr[0] inside a function with const N: usize, the compiler might reject it because N could be 0. You need to prove the size is large enough. You can add a runtime check with assert!(N > 0), or constrain the usage. This is a common gotcha. The compiler is strict about bounds.
Trait bounds. Constraints on const generics are limited. You can do arithmetic like N + 1. You can't easily add arbitrary trait bounds. The compiler needs to evaluate const expressions. Complex bounds may require unstable features or workarounds. Stick to arithmetic and equality checks.
Mismatched types. If you try to assign a [i32; 3] to a variable expecting [i32; 4], you get E0308 (mismatched types). The compiler sees them as unrelated types. You can't coerce between different sizes. You need to copy or convert explicitly.
Don't use const generics for dynamic sizes. If the size changes at runtime, reach for Vec. Const generics are for fixed sizes known at compile time.
Decision matrix
Use const generics for array lengths when the size is fixed and known at compile time. Use const generics for matrices and tensors where dimensions must match for operations to be valid. Use Vec<T> when the size changes at runtime or you need to push and pop without bounds checking overhead. Use slices &[T] when you want to accept a view of data of any length without caring about the underlying storage. Use raw arrays [T; N] without const generics when the size is hardcoded and you don't need a generic function.
Treat the const generic as a contract. If the size matters, put it in the type.