When sizes matter at compile time
You are building a 2D game engine. You need a matrix type to handle coordinate transformations. Some matrices are 2x2 for simple scaling. Some are 3x3 for 2D affine transforms. Some are 4x4 for 3D perspective projection. You could use a Vec<Vec<f64>>, but that means heap allocations, runtime bounds checks, and zero compile-time guarantees that you will not multiply a 3x3 by a 4x4. You want the compiler to catch dimension mismatches before the game even runs. You want zero runtime overhead. That is where const generics step in.
How const generics differ from type generics
Regular generics handle types. You write Vec<T> and the compiler figures out what T is when you call Vec::new(). Const generics handle values. You write Array<const N: usize> and the compiler figures out what N is at compile time. The const keyword tells Rust to treat the parameter as a literal value baked directly into the type itself.
Think of a regular generic as a blank form. You fill in the type later. Think of a const generic as a custom-cut key. The number of teeth is decided when the key is forged, and it will only ever fit locks cut for that exact pattern. The compiler generates separate, specialized versions of your code for every distinct value you pass.
A minimal fixed-size container
/// A fixed-size array wrapper that bakes the length into the type.
struct FixedArray<const N: usize> {
data: [i32; N],
}
impl<const N: usize> FixedArray<N> {
/// Creates a new array filled with zeros.
fn new() -> Self {
// N is known at compile time, so the compiler can allocate exact stack space.
FixedArray { data: [0; N] }
}
/// Returns the compile-time capacity.
fn capacity(&self) -> usize {
// The compiler replaces N with the literal value during monomorphization.
N
}
}
fn main() {
// Explicit turbofish syntax tells the compiler which version to generate.
let small = FixedArray::<3>::new();
let large = FixedArray::<100>::new();
println!("Small holds {} items", small.capacity());
println!("Large holds {} items", large.capacity());
}
The const N: usize syntax is the trigger. It goes inside the angle brackets, right after the struct name. The impl block repeats it so the compiler knows N is available inside the methods. When you call FixedArray::<3>::new(), you are telling the compiler to generate a version of FixedArray where N equals 3. The ::<3> turbofish is explicit, but Rust's type inference often fills it in automatically. Trust the compiler to infer the size when the context is clear.
What the compiler actually does
What happens under the hood is monomorphization. The compiler does not create one generic FixedArray and store a size field at runtime. It creates two completely separate structs. FixedArray<3> gets its own memory layout, its own new function, and its own capacity function. FixedArray<100> gets another set. The N inside capacity() is replaced with the literal 3 or 100 during compilation. The generated machine code contains no variables for the size. It is hardcoded. This is why const generics are called zero-cost abstractions. You get type safety and compile-time guarantees without paying for them at runtime.
The compiler also enforces strict type equality. [i32; 3] and [i32; 4] are different types. You cannot pass a FixedArray<3> to a function expecting FixedArray<4>. The type system catches the mismatch before you can run the program. Treat the size as part of the type identity, not just a property.
Enforcing rules with dimensions
Let's push this further. A common use case is a matrix type that enforces multiplication rules at compile time. Matrix multiplication only works when the columns of the left matrix match the rows of the right matrix. Const generics let you encode that rule directly into the type signature.
/// A 2D matrix with compile-time row and column dimensions.
struct Matrix<const ROWS: usize, const COLS: usize> {
data: [[f64; COLS]; ROWS],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
/// Creates a zero-initialized matrix.
fn zeros() -> Self {
// The compiler calculates the exact memory footprint upfront.
Matrix { data: [[0.0; C]; R] }
}
/// Multiplies this matrix by another.
/// The compiler enforces that self.cols == other.rows.
fn multiply<const K: usize>(self, other: Matrix<C, K>) -> Matrix<R, K> {
// K is a new const parameter for the output columns.
let mut result = Matrix::zeros();
for i in 0..R {
for j in 0..K {
let mut sum = 0.0;
// C is shared between self and other, guaranteeing alignment.
for k in 0..C {
sum += self.data[i][k] * other.data[k][j];
}
result.data[i][j] = sum;
}
}
result
}
}
fn main() {
let a = Matrix::<2, 3>::zeros();
let b = Matrix::<3, 4>::zeros();
// The compiler infers the result type as Matrix<2, 4>.
let _c = a.multiply(b);
}
Notice the multiply signature. It takes self (which is Matrix<R, C>) and other: Matrix<C, K>. The C is shared. If you try to multiply a Matrix<2, 3> by a Matrix<4, 4>, the compiler rejects it immediately. The C in self is 3, but the C in other would need to be 4. They clash. The function returns Matrix<R, K>, so the output dimensions are also baked in. You get mathematical correctness enforced by the type system. Let the compiler do the dimensional analysis for you.
Compile-time assertions
Const generics pair naturally with const assertions. You can validate constraints before the code even compiles. This is useful when you want to restrict a generic to a specific range of values.
/// A hardware register map that only supports power-of-two sizes.
struct RegisterMap<const SIZE: usize> {
registers: [u32; SIZE],
}
impl<const S: usize> RegisterMap<S> {
/// Creates a new map, panicking at compile time if S is not a power of two.
fn new() -> Self {
// The compiler evaluates this expression during compilation.
assert!(S > 0 && (S & (S - 1)) == 0, "Size must be a power of two");
RegisterMap { registers: [0; S] }
}
}
The assert! macro runs at compile time when placed in a const context. If you instantiate RegisterMap<5>, the build fails with a clear message. You do not need to write custom trait bounds or complex macro logic. The compiler catches invalid configurations at the source. Fail fast, fail at compile time.
Where things break
Const generics are powerful, but they have sharp edges. The biggest trap is expecting runtime flexibility. The const parameter must be known at compile time. You cannot read a file, ask the user for a number, and pass that number as a const generic. The compiler will reject it with a "const generics may not be used with runtime values" error. If your size depends on user input, reach for Vec<T> or Box<[T]>.
Another common friction point is trait bounds. You can put const generics on structs and functions, but using them in trait definitions has limits. The compiler sometimes struggles to unify const parameters across different trait implementations. If you write a trait like trait Processor<const N: usize>, you might hit E0277 (trait bound not satisfied) when the compiler cannot prove that two const values are equal across generic boundaries. Keep const generics in concrete types and functions until you have a very specific reason to put them in traits.
You will also see E0308 (mismatched types) constantly when mixing sizes. [u8; 10] is not [u8; 11]. This is a feature, not a bug. The compiler is protecting you from buffer overflows and layout mismatches. When the error appears, check your turbofish syntax or your function signatures. Make sure the const parameters line up exactly. Do not fight the type system here. Align your parameters or change your abstraction.
Choosing the right tool
Use const generics when you need compile-time size guarantees and zero runtime overhead. Use const generics when you want the type system to enforce mathematical or structural rules, like matrix dimensions or fixed buffer capacities. Use const generics when you are building low-level abstractions like ring buffers, fixed-size stacks, or hardware register maps. Reach for Vec<T> when the size is unknown at compile time or changes during execution. Reach for regular type generics (T) when you only need to abstract over data types, not values. Reach for Box<[T]> or &[T] when you need a dynamically sized slice that can be passed around polymorphically.
Convention asides
The community convention for const generic parameters is to use usize for sizes and capacities. You can technically use u32 or i32, but usize aligns with Rust's indexing and memory layout conventions. Another convention is explicit turbofish syntax in examples and tests. Writing Array::<5>::new() makes the size obvious to readers, even though inference often handles it in production code. Finally, keep const generic parameters at the type level. Avoid passing them through layers of generic functions unless necessary. Each layer adds complexity to the compiler's monomorphization process and can increase compile times. Write the size once, let the compiler propagate it.