You use const generics by specifying a generic parameter with the const keyword, allowing the compiler to treat the array length as a compile-time constant that enforces type safety without runtime overhead. This ensures that functions or structs only accept arrays of a specific size, preventing buffer overflows and eliminating the need for manual length checks.
Here is a practical example of a function that only accepts arrays of exactly 3 elements:
fn process_fixed_array<const N: usize>(arr: [i32; N]) -> i32 {
// The compiler knows N is 3 at compile time
if N != 3 {
// This branch is unreachable if called with [i32; 3]
return 0;
}
arr.iter().sum()
}
fn main() {
let small = [1, 2, 3];
let large = [1, 2, 3, 4];
// Compiles successfully
let sum = process_fixed_array(small);
println!("Sum: {}", sum);
// Compile error: expected [i32; 3], found [i32; 4]
// let bad_sum = process_fixed_array(large);
}
You can also combine const generics with structs to create type-safe containers where the size is part of the type itself. This is useful for fixed-size buffers or matrices:
struct Buffer<const CAPACITY: usize> {
data: [u8; CAPACITY],
len: usize,
}
impl<const CAPACITY: usize> Buffer<CAPACITY> {
fn new() -> Self {
Self {
data: [0; CAPACITY],
len: 0,
}
}
fn push(&mut self, val: u8) -> Result<(), String> {
if self.len >= CAPACITY {
return Err("Buffer full".to_string());
}
self.data[self.len] = val;
self.len += 1;
Ok(())
}
}
fn main() {
let mut small_buf: Buffer<4> = Buffer::new();
let mut large_buf: Buffer<10> = Buffer::new();
small_buf.push(10).unwrap();
// large_buf.push(10).unwrap(); // Different type, cannot mix
}
Key takeaways for implementation:
- Syntax: Always use
const N: usize(or other primitive types) in the generic list. - Type Safety:
[T; 3]and[T; 4]are distinct types. The compiler prevents passing the wrong size to a function expecting a specific const generic. - Performance: Since the size is known at compile time, the compiler can optimize loops and memory layout specifically for that size, often resulting in unrolled loops and zero-cost abstractions compared to using
Vecor slices with runtime length checks. - Constraints: You can add constraints like
where N: std::ops::Addif you need to perform arithmetic on the size, though simple equality checks usually suffice for array bounds.