How to Use Const Generics in Rust

Const generics let you parameterise types and functions over compile-time integer values. Great for fixed-size buffers, matrices, and shape-checking at the type level.

When the size is part of the type

You're writing a function that takes a fixed-size byte buffer. Maybe it's a hash, where you know it's exactly 32 bytes. Maybe it's a network packet header, exactly 20. The natural Rust type is [u8; 32] or [u8; 20]. They're stack-allocated, no heap, no length tag, no Vec overhead. Just the bytes.

Now you want to write one function that works for both. Without const generics, you have two unappealing options. Write the function twice with two different signatures. Or take a slice &[u8] and lose the compile-time size information. Const generics give you a third option: a function that's parameterised over the size, with the size still known at compile time.

That phrase, "known at compile time," is the whole point. Generics in Rust come in two flavours. Type generics (<T>) let you parameterise over types: Vec<T>, Option<T>. Const generics (<const N: usize>) let you parameterise over values that are known at compile time. Most often, that's an array length.

The minimal example

The syntax is const NAME: TYPE inside the angle brackets. The type is usually usize because most use cases are array lengths, but it can be any "structural" integer type or bool.

// A function that prints the length of any byte array.
// N is filled in by the compiler at the call site, based on the argument.
fn show<const N: usize>(arr: [u8; N]) {
    // N is just a constant inside the function body.
    println!("got {N} bytes");
}

fn main() {
    let small = [1u8, 2, 3];
    let big = [0u8; 100];

    show(small);   // N is inferred as 3
    show(big);     // N is inferred as 100
}

What's actually happening: the compiler generates two specialised versions of show, one with N = 3 and one with N = 100. They're separate functions in the final binary, just like Vec<i32> and Vec<String> are separate types. This is monomorphisation, the same mechanism that powers type generics.

Why this matters: types that carry their size

Consider a small wrapper struct around a fixed-size byte array.

// A buffer whose length is locked in at compile time.
// The const generic N becomes part of the type's identity.
struct Buffer<const N: usize> {
    bytes: [u8; N],
}

impl<const N: usize> Buffer<N> {
    // Constructor that takes ownership of an array of the right size.
    fn new(bytes: [u8; N]) -> Self {
        Buffer { bytes }
    }

    // Method that returns the size. N is a constant in scope.
    fn len(&self) -> usize { N }
}

fn main() {
    let small: Buffer<4> = Buffer::new([1, 2, 3, 4]);
    let big: Buffer<1024> = Buffer::new([0; 1024]);

    println!("{} {}", small.len(), big.len());   // 4 1024
}

Buffer<4> and Buffer<1024> are different types. You can't accidentally pass one where the other is expected. A function that takes a Buffer<32> will refuse a Buffer<16> at compile time, no if len != 32 { panic } needed at runtime.

error[E0308]: mismatched types
  --> src/main.rs:9:14
   |
9  |     consume(small);
   |     ------- ^^^^^ expected `Buffer<32>`, found `Buffer<4>`

This is the kind of safety you used to get in C++ templates and almost nowhere else. Rust gives it to you with the regular type system, no unsafe, no macros.

A more realistic case: matrix math

A toy example that pays off:

// A matrix with rows known at compile time and cols known at compile time.
// The internal storage is a flat array; we index into it by row * cols + col.
struct Matrix<const R: usize, const C: usize> {
    data: [[f64; C]; R],
}

impl<const R: usize, const C: usize> Matrix<R, C> {
    // Build a zeroed matrix. No allocation: the storage is on the stack.
    fn zeros() -> Self {
        Matrix { data: [[0.0; C]; R] }
    }

    // Read a single cell. Bounds-checked at runtime, but R and C are static.
    fn get(&self, r: usize, c: usize) -> f64 {
        self.data[r][c]
    }
}

// Multiply two matrices. The inner dimensions must match: A is R x K, B is K x C,
// result is R x C. The compiler enforces this at the type level.
fn matmul<const R: usize, const K: usize, const C: usize>(
    a: &Matrix<R, K>,
    b: &Matrix<K, C>,
) -> Matrix<R, C> {
    let mut out = Matrix::<R, C>::zeros();
    for i in 0..R {
        for j in 0..C {
            // Inner loop: sum over the shared dimension K.
            for k in 0..K {
                out.data[i][j] += a.data[i][k] * b.data[k][j];
            }
        }
    }
    out
}

If you try to multiply a Matrix<3, 4> by a Matrix<5, 6>, the compiler refuses. The shared dimension K would have to be both 4 and 5. There's no runtime "shape mismatch" error. The bug is impossible to write.

Where it stops being painless

A few rough edges to know about.

You can't do arithmetic on const generic parameters in stable Rust (yet). You can't write Buffer<{N + 1}> as a type unless you're on nightly with feature(generic_const_exprs). So if you wanted a function "given a buffer of length N, return one of length N+1," you can't express that directly today. You can work around it by introducing a second const parameter M and asserting M == N + 1 at runtime, or by waiting for the feature to stabilise.

Default values for const generics exist (<const N: usize = 8>) but they only work in struct/type definitions, not in function declarations.

The error message when you get a const generic wrong is sometimes cryptic. If you write <const N: usize> and pass a runtime value, you'll see:

error: attempt to use a non-constant value in a constant

The fix is usually to make the value const or to pass it as a literal.

When to reach for it

A rough rule: use const generics when the size of something is known at compile time and you want the type system to enforce that. Stack-allocated buffers, fixed-size hashes, fixed-shape matrices, register-sized SIMD vectors, protocol headers with a known length. The payoff is no heap allocation, no length tag in the type, and shape mismatches caught at compile time.

If the size only becomes known at runtime (read from a config file, comes off the network, decided by user input), const generics aren't the right tool. Use Vec<T> or Box<[T]>. The whole premise of the feature is that the size is a compile-time constant.

If you only have one or two sizes you care about, sometimes hardcoding them is simpler than introducing const generics. [u8; 32] and a method named as_bytes(&self) -> &[u8; 32] is fine. Save the abstraction for cases where you really do want the function to work for many sizes.

Where to go next