Rust Const Generics: Parameterize by Value, Not Just Type
Const generics let Rust types be parameterized by values, not just other types. This allows writing code generic over array sizes, like `Matrix<T, const N: usize>`, ensuring dimensions are checked at compile time.
WHY IT EXISTS Before const generics, creating a truly generic, fixed-size array type was impossible in safe Rust without macros. You could either define a struct for each size (Array32, Array64) or use a dynamically-sized Vec, losing compile-time size guarantees. Const generics solve this by allowing a type's definition to depend on a compile-time constant value.
THE MENTAL MODEL Think of const generics as function arguments, but for your types. A function fn add(a: i32, b: i32) takes values at runtime. A struct Array<T, const N: usize> takes a type T and a value N at compile time. This bakes the value N into the type itself, making Array<u8, 32> and Array<u8, 64> two distinct, incompatible types that the compiler can reason about.
HOW IT WORKS You declare a const generic parameter using the syntax const NAME: Type inside the angle brackets of a generic item, like a struct or function. The allowed types are primitive integers (u8, i32, usize, etc.), bool, and char. The compiler uses this value during monomorphization to create a specialized version of the code for each unique constant provided. This means MyBuffer<16> and MyBuffer<32> result in two different struct definitions in the final binary.
WHEN TO USE IT Use const generics when a value is a fundamental, unchanging property of a type. This is perfect for fixed-size arrays, buffers, matrices, or scientific computing where dimensions are known at compile time. It moves dimension checks from runtime panics to compile-time errors, making your code more robust.
WHEN NOT TO USE IT Do not use const generics for values that need to change at runtime. If you need a collection that can grow or shrink, a Vec<T> is the correct choice. Const generics are for compile-time constants, not dynamic runtime variables. Also, support for complex expressions (like N + 1) in type definitions is still limited, which can be a frustrating constraint.
ONE CANONICAL EXAMPLE A generic, stack-allocated buffer where the size is part of the type. First, the struct definition: struct MyBuffer<T, const SIZE: usize> { data: [T; SIZE] }. Then, an implementation: impl<T, const SIZE: usize> MyBuffer<T, SIZE> { fn len(&self) -> usize { SIZE } }. With this, a MyBuffer<u8, 32> and a MyBuffer<u8, 64> are completely different types, which the compiler can check. This prevents accidentally mixing buffers of different, fixed sizes.
Read the original → doc.rust-lang.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.