How to Use Const Generics for Array-Length Generics

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.

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:

  1. Syntax: Always use const N: usize (or other primitive types) in the generic list.
  2. 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.
  3. 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 Vec or slices with runtime length checks.
  4. Constraints: You can add constraints like where N: std::ops::Add if you need to perform arithmetic on the size, though simple equality checks usually suffice for array bounds.