Step 6 of 6
Const generics: arrays of any length
An array's length is part of its type: [f64; 7] and [f64; 2] are different types. So how do you write one function for arrays of every length? With a const generic parameter: a parameter that stands for a value known at compile time, instead of a type.
fn average<const N: usize>(readings: [f64; N]) -> f64 {
let mut total = 0.0;
for r in readings {
total += r;
}
total / N as f64
}
fn main() {
let week = [12.0, 14.5, 13.0, 15.5, 16.0, 11.0, 12.5];
let weekend = [20.0, 22.0];
println!("week: {:.2}", average(week));
println!("weekend: {:.2}", average(weekend));
}
week: 13.50
weekend: 21.00
const N: usizedeclares the parameter:Nis ausizevalue, fixed at compile time.[f64; N]uses it as the array length, and the body can useNlike a constant (N as f64).- Like type parameters,
Nis inferred from the argument: 7 forweek, 2 forweekend. And like type parameters, eachNgets its own compiled copy.
Const parameters work on structs too, mixed freely with type parameters. Here's a game board whose size is part of its type:
struct Grid<const W: usize, const H: usize> {
cells: [[char; W]; H],
}
impl<const W: usize, const H: usize> Grid<W, H> {
fn new() -> Self {
Grid { cells: [['.'; W]; H] }
}
fn set(&mut self, x: usize, y: usize, c: char) {
self.cells[y][x] = c;
}
fn print(&self) {
for row in &self.cells {
let mut line = String::new();
for c in row {
line.push(*c);
}
println!("{line}");
}
}
}
fn main() {
let mut board: Grid<5, 3> = Grid::new();
board.set(0, 0, '@');
board.set(4, 2, '#39;);
board.print();
}
@....
.....
....$
[['.'; W]; H] builds an array of H rows, each an array of W dots. impl<const W: usize, const H: usize> declares the parameters for the impl block, exactly like impl<T>.
Const generics or slices?
For "a list of things", a slice &[T] is usually the right parameter: it accepts arrays, Vecs and parts of them, of any length decided at run time. Reach for const generics when the length is part of the meaning and the compiler should enforce it:
- an RGB color is exactly
[u8; 3], a 3D position[f64; 3] - a key for a cipher is exactly 32 bytes:
[u8; 32] - two vectors in a dot product must have the same length (this step's exercise)
- a matrix's shape: a 2x3 matrix transposed is a 3x2 matrix
The standard library itself uses const generics: they're why {:?}, == and many other features work on arrays of any length.
Common mistakes
- An array of the wrong length. The length is checked at compile time:
fn brightness(color: [u8; 3]) -> u32 {
color[0] as u32 + color[1] as u32 + color[2] as u32
}
fn main() {
println!("{}", brightness([255, 128, 0, 255]));
}
error[E0308]: mismatched types
| println!("{}", brightness([255, 128, 0, 255]));
| ---------- ^^^^^^^^^^^^^^^^^^ expected an array with a size of 3, found one with a size of 4
- Using
Nwithout declaring it.fn sum(values: [i32; N])givescannot find value `N` in this scope. Declare it:fn sum<const N: usize>(values: [i32; N]). - Const parameters of other types. Only integers,
boolandcharare allowed:`f64` is forbidden as the type of a const generic parameter.
Your task
The dot product of two vectors multiplies matching elements and adds the results: [1, 2, 3] and [4, 5, 6] give 1*4 + 2*5 + 3*6 = 32. The starter only works for length 3. Make dot generic over the length, so the compiler guarantees both arrays have the same length, whatever it is.
Challenge 2
Write fn reversed<T: Copy, const N: usize>(arr: [T; N]) -> [T; N] that returns a reversed copy of the array. Don't use .reverse(): make a mutable copy (let mut out = arr;, allowed because T: Copy makes the array Copy too) and swap elements with out.swap(i, j).
Challenge 3
A Matrix<R, C> has R rows and C columns, both const parameters. Write the transpose method, which turns rows into columns: the result is a Matrix<C, R>, and the element at row r, column c moves to row c, column r. The type system guarantees the new shape.
You'll need to start from a filled array of the new shape: [[0; R]; C].
Previous: Monomorphization: generics cost nothing at run time