🎓 A1Learner
🔍

Rust Const Generics

A const generic is a compile-time value that acts as a parameter. An array carries its length this way, so [i32; 3] and [i32; 4] are different types.

A size in the type

ROWS and COLS are numbers, not types. Grid<i32, 2, 3> holds a [[i32; 3]; 2] — two rows of three. Its count is worked out from the parameters alone:

struct Grid<T, const ROWS: usize, const COLS: usize> {
    cells: [[T; COLS]; ROWS],
}

One function, every size

total never names a size. The N in its signature is read off each array it is handed:

fn total<const N: usize>(arr: [i32; N]) -> i32 {
    let mut sum = 0;
    for n in arr {
        sum += n;
    }
    sum
}
println!("total of 3 = {}", total([1, 2, 3]));
println!("total of 4 = {}", total([10, 20, 30, 40]));

Different sizes, different types

The size is part of the type, so a three-slot array can never slip into a four-slot place:

let three: [i32; 3] = [1, 2, 3];
let four: [i32; 4] = three; // error E0308: mismatched types

A bool can be a parameter too

The parameter need not be a size. Here a bool picks which impl exists. Meter<true> measures in metres. Meter<false> measures in feet. Each value gets only its own unit method:

struct Meter<const METRIC: bool>;
impl Meter<true> {
    fn unit(&self) -> &str { "metres" }
}
impl Meter<false> {
    fn unit(&self) -> &str { "feet" }
}
println!("metric unit = {}", Meter::<true>.unit());
println!("other unit = {}", Meter::<false>.unit());

Arithmetic on N is not allowed yet

A size built by calculating with N — growing an array by one — is beyond stable Rust today. The compiler refuses it with the message below. The working pattern is a second const parameter for the second size:

fn grow<const N: usize>(arr: [u8; N]) -> [u8; N + 1] {
    // refused on stable Rust:
    // error: generic parameters may not be used in const operations
}

Example

struct Grid<T, const ROWS: usize, const COLS: usize> {
    cells: [[T; COLS]; ROWS],
}

impl<T: Copy + Default, const ROWS: usize, const COLS: usize> Grid<T, ROWS, COLS> {
    fn new() -> Grid<T, ROWS, COLS> {
        Grid { cells: [[T::default(); COLS]; ROWS] }
    }
    fn count(&self) -> usize {
        ROWS * COLS
    }
    fn set(&mut self, row: usize, col: usize, value: T) {
        self.cells[row][col] = value;
    }
    fn get(&self, row: usize, col: usize) -> T {
        self.cells[row][col]
    }
}

fn first<const N: usize>(arr: [i32; N]) -> i32 {
    arr[0]
}

fn total<const N: usize>(arr: [i32; N]) -> i32 {
    let mut sum = 0;
    for n in arr {
        sum += n;
    }
    sum
}

fn main() {
    let mut board: Grid<i32, 2, 3> = Grid::new();
    board.set(1, 2, 42);
    println!("cells = {}", board.count());
    println!("corner = {}", board.get(1, 2));
    println!("first of 3 = {}", first([7, 8, 9]));
    println!("first of 5 = {}", first([4, 5, 6, 7, 8]));
    println!("total of 3 = {}", total([1, 2, 3]));
    println!("total of 4 = {}", total([10, 20, 30, 40]));
}

Key points

  • Write const N: usize in the angle brackets.
  • That accepts a compile-time number as a parameter: struct Grid<T, const N: usize>
  • A const parameter may be an integer type, bool or char — values the compiler can hold while it works
  • Arrays of different sizes are different types.
  • Handing a [i32; 3] where a [i32; 4] is wanted fails with error E0308
  • A function generic over N works for every size at once.
  • total adds a [i32; 3] and a [i32; 40] with the same body
  • Use a plain &[T] parameter when the size does not matter: a slice accepts an array of any size
  • Arithmetic on a const parameter inside a type is refused on stable Rust.
  • The refused shape is writing [u8; N + 1].
  • Generic parameters may not be used in const operations.
  • Pass the second size as its own const parameter instead
  • Grid<i32, 2, 3> and Grid<i32, 4, 4> are simply two types, like any others.

In this example, Grid is generic over a type and over two numbers. board is a Grid<i32, 2, 3>, so count multiplies the parameters: 2 times 3 prints cells = 6. The set call stores 42 in the last cell. get reads it back as corner = 42. Below, first and total each take any array. The compiler reads N from the argument. The three-item array gives first of 3 = 7. The five-item array gives first of 5 = 4. total adds both sizes with one body: 6 and 100. Nothing about the sizes was decided while the program ran — every N was settled by the compiler.

Note

A const parameter is settled while compiling and costs nothing while running.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What kind of parameter is the N in struct Grid<T, const N: usize>?