Rust Const Generics: Moving Array Sizes from Runtime to Compile Time
Rust const generics let you move array sizes from runtime values into the type system. Learn how to replace heap-allocated Vecs and typenum boilerplate with compile-time fixed-size types.
02 Sept 2026, 01:03 UTC

The problem: fixed-size data that generics could not describe
If you have written Rust code that operates on arrays of a known length, you have probably felt the friction. An array like [u8; 32] carries its length in the type, which is great for stack allocation and cache locality. But before const generics, a generic function could not preserve that length. You either hard-coded sizes, switched to Vec and accepted heap allocation, or reached for crates such as typenum and generic-array to encode sizes as types.
Const generics, stable since Rust 1.51, close that gap. They let you parameterize functions and structs by compile-time constants, most commonly usize values. The practical result is that array sizes become part of your API contract, checked by the compiler rather than at runtime.
What const generics actually give you
A const generic parameter looks like a type parameter, but it accepts a constant value instead of a type. The simplest example is a function that repeats a value into a fixed-size array:
fn repeat<T, const N: usize>(value: T) -> [T; N]
where
T: Copy,
{
[value; N]
}
Here N is not a runtime argument. It is resolved during compilation, so repeat::<u8, 4>(0) and repeat::<u8, 8>(0) produce different types. The compiler knows the exact length of the returned array, which means downstream code can index without bounds checks when the index is also a compile-time constant.
Worked example: a type-safe matrix
The real power appears when you encode dimensions in a struct. Consider a stack-allocated matrix:
struct Matrix<T, const ROWS: usize, const COLS: usize> {
data: [[T; COLS]; ROWS],
}
impl<T, const ROWS: usize, const COLS: usize> Matrix<T, ROWS, COLS>
where
T: Copy + Default,
{
fn new() -> Self {
Matrix {
data: [[T::default(); COLS]; ROWS],
}
}
fn transpose(&self) -> Matrix<T, COLS, ROWS> {
let mut result = Matrix::<T, COLS, ROWS>::new();
for r in 0..ROWS {
for c in 0..COLS {
result.data[c][r] = self.data[r][c];
}
}
result
}
}
Because ROWS and COLS are part of the type, Matrix<f64, 2, 3> and Matrix<f64, 3, 2> are distinct. You cannot accidentally pass a 2×3 matrix to a function expecting 3×2. The transpose method returns a type with the dimensions swapped, and the compiler enforces that contract. Under the hood, the compiler performs monomorphization: it generates a separate copy of the code for each concrete set of parameters, just as it does for generic type parameters. That specialization is what removes runtime size checks.
Limitations you should know before adopting it
Const generics are not a general-purpose metaprogramming tool. The expressions allowed in a const parameter position are deliberately simple. You cannot use recursion, trait bounds, or complex type families inside the constant expression. Attempting to compute a size through elaborate const arithmetic can trigger compiler bugs, so the safe path is to keep const expressions straightforward and pass literal values or simple const items.
Interoperability is another consideration. Libraries that still rely on typenum or generic-array will not automatically accept const-generic types. You may need to write manual conversion functions at the boundary. Finally, monomorphization has a cost: every distinct combination of type and const parameters generates new machine code. For a small set of sizes this is a performance win, but if you instantiate dozens of sizes across a large codebase, binary size and compile times can grow.
How to verify it works in your project
Start by confirming your toolchain is new enough. In your terminal, run:
rustc --version
You need version 1.51 or newer. Next, add the repeat function above to a scratch binary and run cargo run. To confirm the type safety is real, write a test that tries to assign a Matrix<i32, 2, 3> to a variable of type Matrix<i32, 3, 2>. The compiler should reject it.
If you want to see the monomorphization for yourself, you can inspect expanded code with rustc -Z unstable-options --pretty=expanded, but that flag requires a nightly compiler and is intended for debugging, not production workflows.
Start with one hot path
You do not need to rewrite every collection in your codebase. Pick one place where a fixed-size array currently degrades to a Vec or where a typenum-based type adds friction. Replace it with a const-generic struct, add a compile-fail test for the dimension mismatch, and measure whether removing the runtime bounds check matters for your workload. If the compiler accepts the change and your benchmarks hold, you have a template for the next one.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.