Skip to content

Const Generics

19 min read

Const generics let a type or function be generic over a constant value — most commonly an array length — rather than only over a type. They are how [T; N] works, how a fixed-capacity buffer can be a distinct type from a different-capacity one, and how Rust can verify dimensions like “a 2x3 matrix times a 3x2 matrix” at compile time with zero runtime cost.


A normal generic parameter stands in for a type (Vec<T>). A const generic parameter stands in for a value of some primitive type, written const N: usize. The classic case is the array type [T; N]: the length N is part of the type, so [u8; 4] and [u8; 8] are genuinely different types.

For a TypeScript/JavaScript developer the nearest analogy is a fixed-length tuple type such as readonly [number, number, number]. The crucial difference: TypeScript’s tuple length is a compile-time-only annotation that is fully erased — at runtime it is just a number[], and a length mismatch passes silently. Rust’s const generics are real, monomorphized types that the compiler turns into distinct, separately-optimized machine code, so the length is enforced and exploitable at runtime too.


TypeScript can describe a fixed-length vector with a tuple type and even catch a length mismatch during type-checking:

// A 3-element vector, described with a fixed-length tuple type.
type Vec3 = readonly [number, number, number];
function dot(a: Vec3, b: Vec3): number {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
const a: Vec3 = [1, 2, 3];
const b: Vec3 = [4, 5, 6];
console.log("dot =", dot(a, b)); // dot = 32
// Passing the wrong length is a *compile-time* type error:
dot(a, [4, 5]);
// ~~~~~~ Argument of type '[number, number]' is not assignable
// to parameter of type 'Vec3'. Source has 2 element(s)
// but target requires 3.

That tsc --strict error is real. But the length lives only in the type system. There is no Vec3 value at runtime — it is an ordinary JavaScript array — and a generic helper that takes number[] will happily run on the wrong length:

// Plain JavaScript: the length annotation is gone. Nothing stops a mismatch.
function dot(a, b) {
return a.reduce((sum, x, i) => sum + x * b[i], 0);
}
console.log(dot([1, 2, 3], [4, 5, 6])); // 32
console.log(dot([1, 2, 3], [4, 5])); // NaN — silently wrong

So in TypeScript the length is a hint to the checker, not a property of the runtime value. And tuple types do not let you write code that is generic over an arbitrary fixed length N while still tracking that length — you would end up with a separate Vec2, Vec3, Vec4, or fall back to number[] and lose the guarantee entirely.


In Rust the length is a first-class generic parameter. One definition covers every length, and the length is part of the type at runtime:

use std::ops::{Add, Index, IndexMut};
// A fixed-size mathematical vector, generic over its length N.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vector<const N: usize> {
data: [f64; N],
}
impl<const N: usize> Vector<N> {
fn from_array(data: [f64; N]) -> Self {
Vector { data }
}
fn zero() -> Self {
Vector { data: [0.0; N] }
}
fn dot(&self, other: &Vector<N>) -> f64 {
let mut sum = 0.0;
for i in 0..N {
sum += self.data[i] * other.data[i];
}
sum
}
fn len(&self) -> usize {
N
}
}
// Adding two vectors is legal only when their lengths match: the same N.
impl<const N: usize> Add for Vector<N> {
type Output = Vector<N>;
fn add(self, rhs: Vector<N>) -> Vector<N> {
let mut out = [0.0; N];
for i in 0..N {
out[i] = self.data[i] + rhs.data[i];
}
Vector { data: out }
}
}
impl<const N: usize> Index<usize> for Vector<N> {
type Output = f64;
fn index(&self, i: usize) -> &f64 {
&self.data[i]
}
}
impl<const N: usize> IndexMut<usize> for Vector<N> {
fn index_mut(&mut self, i: usize) -> &mut f64 {
&mut self.data[i]
}
}
fn main() {
let a = Vector::from_array([1.0, 2.0, 3.0]); // Vector<3>
let b = Vector::from_array([4.0, 5.0, 6.0]); // Vector<3>
let sum = a + b;
println!("a + b = {:?}", sum.data);
println!("a . b = {}", a.dot(&b));
println!("len(a) = {}", a.len());
let z = Vector::<4>::zero(); // explicitly Vector<4>
println!("zero(4) = {:?}", z.data);
let c = Vector::from_array([7.0, 8.0]); // Vector<2>
println!("c[1] = {}", c[1]);
}

Real output:

a + b = [5.0, 7.0, 9.0]
a . b = 32
len(a) = 3
zero(4) = [0.0, 0.0, 0.0, 0.0]
c[1] = 8

One Vector<const N: usize> definition serves every length, the length is recoverable at runtime (a.len() returns 3), and — as the next sections show — a length mismatch is a compile error, not a silent NaN.


struct Vector<const N: usize> {
data: [f64; N],
}

The syntax const N: usize declares a generic parameter whose value is a usize constant. It sits in the same <...> angle-bracket list as type parameters and lifetimes. The ordering rule is: lifetimes first, then type and const parameters (which may be interleaved). Inside the body, N is usable anywhere a constant of that type is needed — most importantly as an array length [f64; N], but also in expressions like 0..N.

Note: The type after const (here usize) is the type of the constant value, not a type parameter. The only types currently allowed for a const generic parameter are the integer types, bool, and char — confirmed by the compiler error in the next section.

There are three ways N gets a concrete value:

  1. Inference from an argument. Vector::from_array([1.0, 2.0, 3.0]) passes a [f64; 3], so the compiler infers N = 3.
  2. Explicit turbofish. Vector::<4>::zero() names the value directly, just like Vec::<i32>::new() names a type. Const generic arguments go in the same ::<...> list.
  3. From a type annotation. let z: Vector<4> = Vector::zero(); lets the binding’s declared type drive N.

Const generic arguments are written as a value (4, true, '.') or a standalone const parameter (N). A more complex expression like N + 1 is not allowed in argument position on stable — see Key Differences.

Monomorphization makes each length a real type

Section titled “Monomorphization makes each length a real type”

Rust monomorphizes generics: the compiler stamps out a separate, fully-specialized copy of the code for each concrete set of generic arguments actually used. Vector<3> and Vector<2> compile to two distinct types with two distinct add functions, each with N substituted as a literal. This is the same machinery described in Generic Functions, extended from types to values — and it is the opposite of TypeScript’s type erasure, where Vec3 vanishes before the code runs.

Because N is a compile-time literal inside each monomorphized copy, loops like for i in 0..N have a known trip count, arrays are sized exactly, and the optimizer can unroll or vectorize freely. There is no length field stored at runtime and no heap allocation — Vector<3> is exactly three f64s, 24 bytes, on the stack.

impl<const N: usize> Vector<N> { /* ... */ }

To write methods that work for every N, the impl block itself must introduce the const parameter, exactly as it would introduce a type parameter impl<T> Wrapper<T>. You can also write an impl for one specific length — impl Vector<3> { ... } — to add methods that only make sense at that size (for example, a cross product that exists only for 3-vectors).

The array type [T; N] is the most pervasive const-generic type in Rust, and the standard library exposes const-generic combinators over it:

fn main() {
// `<[T; N]>::map` keeps the length in the type: [i32; 4] -> [i32; 4].
let doubled = [1, 2, 3, 4].map(|x| x * 2);
println!("map double = {:?}", doubled);
// `std::array::from_fn` builds an array, inferring N from the target type.
let squares: [usize; 5] = std::array::from_fn(|i| i * i);
println!("squares = {:?}", squares);
// Arrays own their elements and yield them by value via IntoIterator
// (stable since the 2021 edition).
let odd_sum: i32 = [1, 2, 3].into_iter().filter(|&x| x % 2 == 1).sum();
println!("odd sum = {}", odd_sum);
}

Real output:

map double = [2, 4, 6, 8]
squares = [0, 1, 4, 9, 16]
odd sum = 4

std::array::from_fn is the idiomatic way to initialize a [T; N] element-by-element when T is not Copy (you cannot write [value; N] unless value: Copy).


AspectTypeScript fixed-length tupleRust const generics
What it parameterizes overA literal tuple shape ([number, number, number])Any constant N of an integer / bool / char type
Generic over an arbitrary lengthNo — you write Vec2, Vec3, … separatelyYes — one Vector<const N: usize> covers all
Runtime representationErased; just a JS arrayA real, distinct, monomorphized type per N
Length recoverable at runtimeOnly via arr.length (no type involvement)N is a compile-time constant usable in code
Mismatch detectionCompile-time only; silent at runtimeCompile-time and structurally distinct types
CostZero (erased)Zero at runtime; larger binary (monomorphization)
Allowed parameter “types”Any tuple of typesIntegers, bool, char only (stable)

Stable vs. nightly: the const-generics roadmap

Section titled “Stable vs. nightly: the const-generics roadmap”

What shipped in stable Rust (often called min_const_generics) is deliberately conservative. The rules you can rely on today:

  • Const generic parameters may be integers, bool, or char.
  • A const generic argument must be either a literal/const value, or a standalone const parameter (just N, not N + 1 or N * 2).

Computing with const parameters — e.g. returning [T; N + 1], or splitting [T; N] into [[T; CHUNK]; N / CHUNK] — needs the unstable generic_const_exprs feature and a nightly compiler. The standalone-argument restriction is exactly what produces the error in the next section. (For why some powerful type-system features stay unstable, see the sibling note on Specialization.)


Pitfall 1: Trying to do arithmetic on a const parameter

Section titled “Pitfall 1: Trying to do arithmetic on a const parameter”

A natural first instinct — return an array one element longer — does not compile on stable:

// does not compile: const arithmetic in array sizes is unstable.
fn push_one<const N: usize>(arr: [i32; N], x: i32) -> [i32; N + 1] {
let mut out = [0; N + 1];
out[..N].copy_from_slice(&arr);
out[N] = x;
out
}

The real compiler emits the same error for each use of N + 1 (here, both the return type and the [0; N + 1] array); the representative one is:

error: generic parameters may not be used in const operations
--> src/main.rs:2:61
|
2 | fn push_one<const N: usize>(arr: [i32; N], x: i32) -> [i32; N + 1] {
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

Tip: When you truly need a derived length, the stable workarounds are: return a Vec<T> (heap, dynamic length), take/return slices &[T], or — if N is small and known — hard-code the specific sizes. Reaching for nightly generic_const_exprs should be a last resort because the feature is incomplete and its rules still change.

Pitfall 2: Using a forbidden parameter type

Section titled “Pitfall 2: Using a forbidden parameter type”

Integers, bool, and char work; floating-point and arbitrary structs do not:

struct Flag<const ON: bool>; // ok
struct Grid<const W: usize, const FILL: char>; // ok
struct Scaled<const FACTOR: f64>; // does not compile

The real error:

error: `f64` is forbidden as the type of a const generic parameter
--> src/main.rs:6:29
|
6 | struct Scaled<const FACTOR: f64>;
| ^^^
|
= note: the only supported types are integers, `bool`, and `char`

Floats are disallowed because const generics rely on structural equality of values to decide when two types are the same, and IEEE-754 equality (NaN != NaN, 0.0 == -0.0) does not give a clean, total notion of equality.

If nothing at the call site pins down N, you get an inference error — not a default of zero:

fn make<const N: usize>() -> [u8; N] {
[0; N]
}
fn main() {
let arr = make(); // does not compile: N is unknown
println!("{:?}", arr);
}

The real error tells you exactly how to fix it (the compiler reports E0284 once per unconstrained use, so you may also see a follow-on copy pointing at the println!; the key one is the first):

error[E0284]: type annotations needed for `[u8; _]`
--> src/main.rs:7:9
|
7 | let arr = make();
| ^^^ ------ type must be known at this point
|
note: required by a const generic parameter in `make`
--> src/main.rs:1:9
|
1 | fn make<const N: usize>() -> [u8; N] {
| ^^^^^^^^^^^^^^ required by this const generic parameter in `make`
help: consider giving `arr` an explicit type, where the value of const parameter `N` is specified
|
7 | let arr: [_; N] = make();
| ++++++++

The fix is to annotate (let arr: [u8; 4] = make();) or turbofish (make::<4>()).

Pitfall 4: Expecting different lengths to be interchangeable

Section titled “Pitfall 4: Expecting different lengths to be interchangeable”

Because each N is a distinct type, mixing them is a type error — which is the whole point, but it surprises developers coming from number[]:

let a = Vector { data: [1.0, 2.0, 3.0] }; // Vector<3>
let b = Vector { data: [4.0, 5.0] }; // Vector<2>
let _ = a + b; // does not compile

The real error:

error[E0308]: mismatched types
--> src/main.rs:20:17
|
20 | let _ = a + b; // mismatched lengths
| ^ expected `3`, found `2`
|
= note: expected struct `Vector<3>`
found struct `Vector<2>`

Where TypeScript catches this only during type-checking (and plain JavaScript not at all, producing NaN), Rust makes the two lengths structurally different types, so the guarantee holds all the way through to the running binary.


  • Prefer [T; N] over Vec<T> only when the size is genuinely fixed and known at compile time. Const generics give you stack allocation, no length field, and aggressive optimization — but they also multiply monomorphized code. If lengths vary at runtime, a Vec<T> or slice is the right tool. See Section 21: Performance for the binary-size and inlining trade-offs of monomorphization.

  • Encode invariants in the type when a mismatch would be a real bug. Matrix and vector dimensions, fixed protocol-frame sizes, and ring-buffer capacities are excellent candidates: a wrong size becomes a compile error instead of a runtime panic or silent corruption.

  • Use std::array::from_fn to initialize non-Copy arrays. [None; N] fails when the element is not Copy; std::array::from_fn(|_| None) works for any N.

  • Give const parameters defaults when one size dominates. struct Buffer<T, const N: usize = 32> lets callers write Buffer<u8> for the common case and Buffer<u8, 8> to override:

    struct Buffer<T, const N: usize = 32> {
    data: [T; N],
    }
    impl<T: Default + Copy, const N: usize> Buffer<T, N> {
    fn new() -> Self {
    Buffer { data: [T::default(); N] }
    }
    }
    fn main() {
    let a: Buffer<u8> = Buffer::new(); // default N = 32
    let b: Buffer<u8, 8> = Buffer::new(); // override
    println!("{} {}", a.data.len(), b.data.len()); // 32 8
    }
  • Reach for ecosystem crates instead of hand-rolling. Const generics power heapless (fixed-capacity, allocation-free Vec/String/maps for embedded and real-time code) and the linear-algebra crate nalgebra (statically-sized Matrix<R, C>). Don’t reinvent a fixed-capacity collection if one of those fits.

    # cargo add heapless
    [dependencies]
    heapless = "0.9.3"
    use heapless::Vec;
    fn main() {
    // heapless::Vec<T, const N: usize>: a fixed-capacity, no-heap vector.
    let mut v: Vec<u8, 4> = Vec::new();
    v.push(1).unwrap();
    v.push(2).unwrap();
    v.push(3).unwrap();
    v.push(4).unwrap();
    // Capacity is part of the type; the fifth push has nowhere to go.
    assert_eq!(v.push(5), Err(5));
    println!("len={} capacity={}", v.len(), v.capacity());
    }

    Real output:

    len=4 capacity=4

A statically-sized matrix where the type system enforces multiplication shapes. A 2x3 matrix can only multiply a 3xK matrix — the shared inner dimension 3 must match, and the result type 2xK is computed by the compiler. A shape mismatch never reaches runtime.

// An R x C matrix whose dimensions live in the type.
#[derive(Debug, Clone, Copy)]
struct Matrix<const R: usize, const C: usize> {
rows: [[f64; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(rows: [[f64; C]; R]) -> Self {
Matrix { rows }
}
// (R x C) * (C x K) = (R x K). The shared dimension C is enforced by the
// type system, so a shape mismatch is a compile error, never a panic.
fn mul<const K: usize>(&self, other: &Matrix<C, K>) -> Matrix<R, K> {
let mut out = [[0.0; K]; R];
for i in 0..R {
for j in 0..K {
for k in 0..C {
out[i][j] += self.rows[i][k] * other.rows[k][j];
}
}
}
Matrix { rows: out }
}
}
fn main() {
let a: Matrix<2, 3> = Matrix::new([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let b: Matrix<3, 2> = Matrix::new([[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]]);
let c = a.mul(&b); // inferred as Matrix<2, 2>
println!("{:?}", c.rows);
}

Real output:

[[58.0, 64.0], [139.0, 154.0]]

If you try to multiply incompatible shapes — say a Matrix<2, 3> by a Matrix<2, 4> — the compiler rejects it because the method requires the right-hand operand to be Matrix<C, K> where C = 3 (the full message also prints a note: method defined here pointing at mul, trimmed here for brevity):

error[E0308]: mismatched types
--> src/main.rs:23:19
|
23 | let _ = a.mul(&b);
| --- ^^ expected `3`, found `2`
| |
| arguments to this method are incorrect
|
= note: expected reference `&Matrix<3, _>`
found reference `&Matrix<2, 4>`

This is “make illegal states unrepresentable” applied to numeric code: dimension bugs that would be runtime exceptions in NumPy or a JavaScript math library become compile errors in Rust.


Cross-links within this guide:


Difficulty: Beginner

Objective: Practice writing methods on a const-generic type and using N inside the body.

Instructions: Define struct Square<const N: usize> { cells: [[f64; N]; N] }. Implement Square::<N>::identity() that returns the N x N identity matrix (1.0 on the diagonal, 0.0 elsewhere), and a trace(&self) -> f64 method that sums the diagonal. Print trace of a 3x3 identity (it should be 3).

Solution
#[derive(Debug)]
struct Square<const N: usize> {
cells: [[f64; N]; N],
}
impl<const N: usize> Square<N> {
fn identity() -> Self {
let mut cells = [[0.0; N]; N];
for i in 0..N {
cells[i][i] = 1.0;
}
Square { cells }
}
fn trace(&self) -> f64 {
(0..N).map(|i| self.cells[i][i]).sum()
}
}
fn main() {
let id = Square::<3>::identity();
println!("trace I3 = {}", id.trace()); // trace I3 = 3
}

Difficulty: Intermediate

Objective: Build a small allocation-free collection whose capacity is a type parameter, handling the “full” case without panicking.

Instructions: Implement struct Stack<T, const CAP: usize> backed by [Option<T>; CAP] and a length counter. Provide new(), push(&mut self, value: T) -> Result<(), T> (returning Err(value) when full), pop(&mut self) -> Option<T>, and is_full(&self) -> bool. Note that you must use std::array::from_fn(|_| None) to initialize the array, since T is not Copy.

Solution
struct Stack<T, const CAP: usize> {
items: [Option<T>; CAP],
len: usize,
}
impl<T, const CAP: usize> Stack<T, CAP> {
fn new() -> Self {
Stack { items: std::array::from_fn(|_| None), len: 0 }
}
fn push(&mut self, value: T) -> Result<(), T> {
if self.len == CAP {
return Err(value);
}
self.items[self.len] = Some(value);
self.len += 1;
Ok(())
}
fn pop(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
self.len -= 1;
self.items[self.len].take()
}
fn is_full(&self) -> bool {
self.len == CAP
}
}
fn main() {
let mut s: Stack<&str, 2> = Stack::new();
assert!(s.push("a").is_ok());
assert!(s.push("b").is_ok());
assert_eq!(s.push("c"), Err("c")); // full
assert!(s.is_full());
println!("pop = {:?}", s.pop()); // pop = Some("b")
}

Difficulty: Intermediate

Objective: Use a const generic to size a coefficient array and evaluate efficiently.

Instructions: Define struct Polynomial<const N: usize> { coeffs: [f64; N] } where coeffs[i] is the coefficient of x^i. Implement new(coeffs: [f64; N]) and eval(&self, x: f64) -> f64 using Horner’s method (fold from the highest-degree coefficient down). Verify that 2 + 3x + x^2 evaluated at x = 2 gives 12.

Solution
struct Polynomial<const N: usize> {
coeffs: [f64; N], // coeffs[i] is the coefficient of x^i
}
impl<const N: usize> Polynomial<N> {
fn new(coeffs: [f64; N]) -> Self {
Polynomial { coeffs }
}
// Horner's method: ((... ) * x + c1) * x + c0.
fn eval(&self, x: f64) -> f64 {
self.coeffs.iter().rev().fold(0.0, |acc, &c| acc * x + c)
}
}
fn main() {
// 2 + 3x + x^2, at x = 2 -> 2 + 6 + 4 = 12
let p = Polynomial::new([2.0, 3.0, 1.0]);
println!("p(2) = {}", p.eval(2.0)); // p(2) = 12
}