When size matters at compile time
You are building a game inventory system. Each character can hold exactly twelve items. In Python or JavaScript, you would create a list and add a runtime check to prevent pushing a thirteenth item. The check happens every time you call the push method. The CPU evaluates the condition, branches, and continues. In Rust, you can eliminate that runtime check entirely. You can bake the number twelve directly into the type itself. The compiler will refuse to compile the program if you ever try to store a thirteenth item. This is what const generics do.
The concept: baking values into types
Standard generics in Rust handle types. You write Vec<T> and the compiler generates code that works for Vec<String>, Vec<i32>, or Vec<MyStruct>. The T is a placeholder for a type. Const generics flip the script. Instead of a placeholder for a type, you get a placeholder for a value. That value must be known at compile time. The most common type for these values is usize.
Think of type generics like a universal socket adapter. It accepts any plug shape, but the adapter itself has to handle the variation at runtime or through heavy code generation. Const generics are like a custom-molded socket. You specify the exact dimensions before you pour the concrete. Once it hardens, only that exact shape fits. The compiler generates a completely separate version of your struct or function for each constant value you pass in. There is no runtime overhead. There is no dynamic dispatch. The size is part of the type signature.
A minimal example
The syntax looks like a standard generic, but with the const keyword and a concrete type annotation.
/// A fixed-size array wrapper that bakes its length into the type.
struct FixedArray<const N: usize> {
/// The underlying data. The size N is known at compile time.
data: [i32; N],
}
fn main() {
// Instantiate with N = 10. The compiler substitutes 10 everywhere.
let arr1 = FixedArray { data: [0; 10] };
// Instantiate with N = 5. This is a completely different type.
let arr2 = FixedArray { data: [1; 5] };
// The compiler knows the exact memory layout for each.
println!("First array size: {}", std::mem::size_of_val(&arr1.data));
println!("Second array size: {}", std::mem::size_of_val(&arr2.data));
}
Notice the <const N: usize> syntax. The const keyword tells the compiler this is a value parameter, not a type parameter. The usize restricts it to unsigned integers, which is the standard for sizes and indices. You can use other primitive types like u8 or bool, but usize covers ninety percent of real-world use cases. The compiler treats FixedArray::<10> and FixedArray::<5> as entirely unrelated types. You cannot assign one to the other, even though they hold the same underlying data type.
How the compiler handles it
When you compile this code, the Rust compiler performs monomorphization. It scans your code, finds every unique combination of generic parameters, and generates specialized machine code for each one. For FixedArray::<10>, it allocates exactly forty bytes on the stack (ten i32 values). For FixedArray::<5>, it allocates twenty bytes. The binary contains two distinct struct layouts. There is no hidden pointer to a heap allocation. There is no length field stored alongside the data at runtime. The length lives in the type system.
This has a direct impact on performance. Array bounds checking in Rust normally requires a runtime comparison: if index >= length { panic!() }. When the length is a const generic, the compiler often optimizes the check away entirely if it can prove the index is within bounds. Even when it cannot prove it, the constant is available for loop unrolling and other aggressive optimizations. The CPU never has to fetch a length variable from memory. The branch predictor never has to guess.
Const evaluation is the engine behind this. The compiler runs a simplified interpreter during compilation to resolve const expressions. It can handle arithmetic, array indexing, and basic control flow. It cannot handle heap allocation, I/O, or complex trait resolution. If your const expression tries to do something the compiler cannot evaluate statically, you will get a hard error. The compiler is strict about what counts as a constant.
Realistic usage: compile-time dimension checks
Const generics shine when you need to enforce relationships between values at compile time. Matrix multiplication is a classic example. You cannot multiply a 3x4 matrix by a 2x3 matrix. The inner dimensions must match. With const generics, you can encode those dimensions into the type and let the compiler reject invalid operations before the program ever runs.
/// A 2D matrix with compile-time row and column counts.
struct Matrix<const ROWS: usize, const COLS: usize> {
/// Flattened data stored in row-major order.
data: [f64; ROWS * COLS],
}
impl<const R1: usize, const C1: usize, const C2: usize> Matrix<R1, C1> {
/// Multiplies this matrix by another. The compiler enforces dimension matching.
fn multiply(&self, other: &Matrix<C1, C2>) -> Matrix<R1, C2> {
// Pre-allocate the result matrix. Size is known at compile time.
let mut result = Matrix { data: [0.0; R1 * C2] };
// Nested loops compute the dot product for each cell.
for r in 0..R1 {
for c in 0..C2 {
let mut sum = 0.0;
for k in 0..C1 {
// Index calculation uses the const generics directly.
let idx_a = r * C1 + k;
let idx_b = k * C2 + c;
sum += self.data[idx_a] * other.data[idx_b];
}
result.data[r * C2 + c] = sum;
}
}
result
}
}
fn main() {
// A 2x3 matrix
let m1 = Matrix { data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] };
// A 3x2 matrix
let m2 = Matrix { data: [7.0, 8.0, 9.0, 10.0, 11.0, 12.0] };
// This compiles. C1 (3) matches the rows of m2.
let product = m1.multiply(&m2);
// This would fail to compile if you tried to multiply mismatched dimensions.
// The type system catches the error at build time.
}
The multiply method signature contains the constraint. self has dimensions R1 x C1. other has dimensions C1 x C2. The C1 appears in both positions. If you try to pass a matrix with a different inner dimension, the type checker rejects it immediately. You do not need runtime assertions. You do not need to document the requirement. The code itself enforces the rule. The compiler generates a separate multiply function for every unique combination of R1, C1, and C2 that your code actually uses. Unused combinations are never compiled.
Pitfalls and compiler boundaries
Const generics are powerful, but they are not a replacement for every dynamic structure. The compiler enforces strict rules about what counts as a constant. You cannot pass a runtime variable as a const generic argument. If you try, the compiler rejects it with E0435 (cannot find value in this scope) or a similar "expected constant, found variable" error. The value must be a literal, a const item, or a const expression that the compiler can fully evaluate during compilation.
Trait bounds with const generics are also restricted. You cannot currently write trait MyTrait<const N: usize> and use it as a trait object or in generic bounds without nightly features. The stable compiler requires const generic parameters to be concrete values when crossing trait boundaries. If you need polymorphism over different sizes, you will likely need to use regular type generics or dynamic dispatch instead.
Another common trap is assuming const generics work seamlessly with complex trait implementations. The compiler sometimes struggles to unify const generic parameters across multiple generic functions. You may encounter E0277 (trait bound not satisfied) when the compiler cannot prove that a const expression matches another const expression, even if they are mathematically identical. Work around this by using explicit const items for your dimensions rather than inline arithmetic. The compiler's const evaluator is deterministic but not infinitely clever. Give it simple, explicit values.
Convention aside: the Rust community strongly prefers explicit const names like const LEN: usize or const CAPACITY: usize over single letters like const N: usize in public APIs. Single letters are acceptable in internal math-heavy code, but descriptive names make the type signature readable without hovering over it in an IDE. Stick to descriptive names for library code. It saves future maintainers from decoding cryptic signatures.
When to reach for const generics
Use const generics when you need to encode size, capacity, or dimension directly into a type signature to eliminate runtime checks. Use const generics when you are building fixed-size data structures like matrices, fixed queues, or cryptographic buffers where the size is known at compile time. Use const generics when you want the compiler to enforce relationships between multiple values, such as matching matrix dimensions or array slice bounds. Reach for regular type generics when the data size varies at runtime or when you need trait object polymorphism. Reach for Vec<T> or Box<[T]> when you need dynamic resizing or heap allocation with unknown sizes. Pick const generics only when the compile-time guarantee outweighs the added complexity in your API surface.