Introduction
Const generics enable several powerful patterns: type-level integers for dimensional analysis, fixed-capacity data structures, and compile-time size validation. This lesson covers real-world patterns you will encounter in production Rust code.
Key Concepts
- Type-Level Integers: Using const generics to encode numeric properties in the type system.
- Fixed-Capacity Structures: Data structures whose maximum size is a const generic, avoiding heap allocation.
- Compile-Time Assertions: Using const generics to enforce invariants at compile time.
Real World Context
Embedded Rust uses fixed-size buffers (heapless::Vec<T, N>) to avoid heap allocation. The nalgebra crate uses const generics for matrix dimensions. Network protocols use const generics to define packet structures with known sizes.
Deep Dive
Fixed-Capacity Stack Vec
ruststruct StackVec<T, const N: usize> { data: [std::mem::MaybeUninit<T>; N], len: usize, } impl<T, const N: usize> StackVec<T, N> { fn new() -> Self { StackVec { data: std::array::from_fn(|_| std::mem::MaybeUninit::uninit()), len: 0, } } fn push(&mut self, value: T) -> Result<(), T> { if self.len >= N { return Err(value); // Compile-time capacity! } self.data[self.len] = std::mem::MaybeUninit::new(value); self.len += 1; Ok(()) } }
Bool Const Generics for Feature Flags
ruststruct Logger<const VERBOSE: bool>; impl Logger<true> { fn log(&self, msg: &str) { println!("[VERBOSE] {msg}"); } } impl Logger<false> { fn log(&self, _msg: &str) { // No-op in non-verbose mode } }
The compiler eliminates the non-verbose path entirely — zero runtime cost for disabled logging.
Array Initialization with std::array::from_fn
std::array::from_fn uses const generics to create arrays of any size from a closure:
rust// Create an array of N elements using a closure let squares: [i32; 5] = std::array::from_fn(|i| (i * i) as i32); assert_eq!(squares, [0, 1, 4, 9, 16]); // Works with any const generic size fn make_identity<const N: usize>() -> [[f64; N]; N] { std::array::from_fn(|i| std::array::from_fn(|j| if i == j { 1.0 } else { 0.0 })) } let m: [[f64; 3]; 3] = make_identity();
Common Pitfalls
- Stack overflow with large const generics —
StackVec<u8, 1_000_000>allocates 1 MB on the stack. Use heap allocation for large buffers. - Forgetting that each
Nvalue is a different type —Buffer<64>andBuffer<128>are completely different types and cannot be mixed.
Best Practices
- Use const generics for embedded and no-std code — Fixed-capacity structures avoid heap allocation entirely.
- Combine with traits for size-independent APIs — Implement traits on
Buffer<N>for anyNso that generic code can work with any size.
Summary
- Const generics enable fixed-capacity, stack-allocated data structures.
- Bool const generics can serve as compile-time feature flags with zero runtime cost.
- Each distinct const value produces a different type.
std::array::from_fndemonstrates const generics in the standard library for array initialization.
Code Examples
rust
// Bool const generic for compile-time feature toggling
struct Connection<const ENCRYPTED: bool>;
impl Connection<true> {
fn send(&self, data: &[u8]) {
let encrypted = encrypt(data);
transmit(&encrypted);
}
}
impl Connection<false> {
fn send(&self, data: &[u8]) {
transmit(data); // No encryption overhead
}
}
// The compiler generates only the code path you use
fn encrypt(data: &[u8]) -> Vec<u8> { todo!() }
fn transmit(data: &[u8]) { todo!() }