Skip to content

Rust Advanced Generics: Const Generics, const fn and PhantomData

On the Generics page, a generic parameter was always a type: Pair<T> could hold i32s or &strs. This page shows three tools that push the same idea further:

  • Const generics let a number be a generic parameter, like the 3 in [i32; 3].
  • const fn lets the compiler run your function while it builds your program, so the answer is ready before the program starts.
  • PhantomData lets a type carry a label (like “this is a user ID” or “this length is in feet”) without storing any extra data.

Think of an egg carton. A 6-egg carton and a 12-egg carton are different products, even though both are “egg cartons”. The size is part of what they are. Const generics let you say that in Rust. Take this page slowly: each section stands on its own, and you can come back to the later ones when you need them.

Const generics: a number as a generic parameter

Section titled “Const generics: a number as a generic parameter”

In Rust, the length of an array is part of its type. [u32; 3] and [u32; 4] are as different as i32 and String. A function that takes [u32; 3] won’t accept four items:

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

A const generic fixes this. You write const N: usize in the angle brackets, and the function works for an array of any length:

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// Inside the function, N is just a normal number
format!("{N} items cost {sum}")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

Output:

3 items cost 16
5 items cost 30
▶ Try it Yourself

Read <const N: usize> as “for any number N of type usize”. Rust works out N from the array you pass in, just like it works out T for normal generics. And just like normal generics, the compiler makes a separate copy of summary for each length you use, so there’s no runtime cost.

To be honest: for this little function, a slice (prices: &[u32]) would work just as well and is simpler. See Slices. Const generics really pay off when the length must be part of a type: stored in a struct without a heap allocation, or used in a return type. That’s next.

Structs with const generics: a grid of seats

Section titled “Structs with const generics: a grid of seats”

Picture a cinema seating plan: some rows, each with the same number of seats. Here is a matrix whose number of rows R and columns C are both part of its type:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// Rows become columns: an R x C matrix turns into a C x R matrix
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

Output:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ Try it Yourself

Look at the return type of transpose: Matrix<C, R>. The compiler knows that flipping a 2x3 matrix gives a 3x2 one. If you made a mistake inside, such as building a matrix of the wrong shape, it wouldn’t compile. The whole grid lives inside the struct, with no Vec and no heap. The impl<const R: usize, const C: usize> line works like impl<T>: it says “these methods exist for every size”.

A practical example: a fixed-size ring buffer

Section titled “A practical example: a fixed-size ring buffer”

A car dashcam keeps only the last few minutes of video. When it’s full, the newest recording overwrites the oldest. That’s a ring buffer, and it’s a perfect fit for const generics: the capacity is fixed, so you can store the items in an array.

This one is generic over both a type T and a size N, so the impl line says impl<T, const N: usize>:

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // where the next push goes
len: usize, // how many items are stored (never more than N)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // overwrites the oldest item when full
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// The stored items, oldest first
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// Keep only the last 3 temperature readings
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("after {temp}: {:?}", recent.to_vec());
}
}

Output:

after 18: [18]
after 19: [18, 19]
after 21: [18, 19, 21]
after 24: [19, 21, 24]
after 22: [21, 24, 22]
▶ Try it Yourself

[T::default(); N] fills the array with N copies of a default value (0 for i32). Repeating a value like that needs T: Copy, which is why the impl asks for Copy + Default. A RingBuffer<i32, 3> and a RingBuffer<i32, 60> are different types, so you can never pass a 3-slot buffer where a 60-slot one is expected. If you want the standard library’s growable version instead, see VecDeque & BinaryHeap.

A const parameter can have a default, written const N: usize = 4. You can also pass a named constant, or an expression in curly braces:

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // no number: the default, 4
let page = Buffer::<PAGE>::new(); // a named constant
let double = Buffer::<{ PAGE * 2 }>::new(); // an expression needs { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

Output:

4 8 16
▶ Try it Yourself

The default only kicks in when you write the type without a number, like let small: Buffer. A bare let small = Buffer::new(); fails with error[E0284]: type annotations needed for Buffer<_>, because Rust doesn’t guess a default from an expression.

Const generics on stable Rust are useful, but deliberately limited. It helps to know the edges before you hit them:

  • Allowed types: only integers (usize, u8, i32, …), bool and char. Trying const NAME: &'static str gives “&'static str is forbidden as the type of a const generic parameter”.
  • No math with a generic parameter in a type. [f32; N * 2] inside a generic struct or signature is an error (“generic parameters may not be used in const operations”). Plain N is fine, and so is N * 2 inside a function body, where N is just a value. The feature that would allow N * 2 in types (generic_const_exprs) is still nightly-only.
  • Checking a value. You can’t write a bound like “N must be more than 0”. The closest you get is an inline const block in a function: const { assert!(N > 0, "need at least one slot") };. It fails when you build the program with a bad N (cargo check may not catch it).

Some shops print their price lists once, instead of working out every price at the till. A const fn is a function the compiler is allowed to run while it builds your program, so the answer is “printed” into the program ahead of time.

You’ve already met const items on the Variables page. Their value must be known at compile time, so normally you can only use literals and simple math. With const fn, you can call your own functions there too:

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // worked out by the compiler
fn main() {
// Array lengths must be known at compile time, and CELLS is
let board = [0u8; CELLS];
println!("a {BOARD_SIDE}x{BOARD_SIDE} board has {} cells", board.len());
println!("square(12) at runtime is {}", square(12));
}

Output:

a 4x4 board has 16 cells
square(12) at runtime is 144
▶ Try it Yourself

A const fn is still a normal function. square(12) in main is an ordinary call at runtime. It only has to run at compile time when it’s used in a const context: a const or static item, an array length, a const generic argument, or a const { ... } block.

A classic use is a lookup table: work out a list of values once, at compile time, and just read it at runtime. Here the compiler builds the “day of the year each month starts on” table for a non-leap year:

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// Built once by the compiler. At runtime it's just 12 numbers.
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("March 1 is day {}", day_of_year(3, 1));
println!("December 25 is day {}", day_of_year(12, 25));
}

Output:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
March 1 is day 60
December 25 is day 359
▶ Try it Yourself

Notice the while loop instead of a for loop. That’s not a style choice. Here is what you can use inside a const fn today:

  • let and let mut, arithmetic, comparisons, if, match, loop and while
  • arrays, tuples, structs and enums, and indexing like table[month]
  • calls to other const fns (many standard ones are const, like u32::pow or slice.len())
  • &mut references and floating-point math (stable since Rust 1.83 and 1.82)
  • panic! and assert!, which become compile errors when hit at compile time

And what you can’t use yet: for loops (they need the Iterator trait, and trait methods aren’t const on stable), calls to normal functions such as String::from or println!, and anything that allocates on the heap, like pushing to a Vec.

const or static? Both are global values that never change, but they’re stored differently:

const static
Where it lives copied into every place you use it one fixed place in memory
Address may differ each time always the same, lives for the whole program ('static)
Can be mutable never static mut exists, but needs unsafe
Use it for numbers, small tables, settings a large table you want stored once, or a global you need a reference to

When in doubt, use const. If you need a global you can change, use a static with an atomic or a Mutex inside instead of static mut. See Shared State.

Here’s a bug that happens in real systems all the time. User IDs and order IDs are both just numbers, so nothing stops you passing a user ID to cancel_order. You’d like the type to carry a label: Id<User> versus Id<Order>. Your first try might be this:

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

Rust refuses a type parameter that no field uses. The fix is the one the compiler suggests. std::marker::PhantomData<T> is a field that says “this struct is connected to T” but holds nothing. It takes zero bytes:

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // zero bytes, just a label
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "Amara" } else { "unknown" }
}
fn cancel_order(id: &Id<Order>) {
println!("cancelling order #{}", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("user #{} is {}", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("an Id is {} bytes", std::mem::size_of::<Id<User>>());
}

Output:

user #7 is Amara
cancelling order #7
an Id is 4 bytes
▶ Try it Yourself

Both IDs hold the number 7, and both are 4 bytes, exactly like a plain u32. But now add cancel_order(&user_id); as the last line of main:

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

The mix-up is caught before the program ever runs. PhantomData is a value as well as a type, which is why you write _marker: PhantomData when you build the struct. The leading _ is just a common naming habit for “you never read this field”.

In 1999, NASA lost the Mars Climate Orbiter because one piece of software produced numbers in imperial units and another expected metric. Both were “just numbers”. With PhantomData, the unit becomes part of the type, and one generic Length<U> covers every unit:

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// One impl for every unit, but both sides must use the same unit
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("climb: {:.1} m", climb_m.value);
let total = run + climb_m;
println!("total: {:.1} m", total.value);
}

Output:

climb: 304.8 m
total: 954.8 m
▶ Try it Yourself

Write run + climb (forgetting to_meters()) and Rust refuses:

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

On the Advanced Traits page, Meters and Centimeters were two separate structs, each with its own Add impl. Here, one impl<U> Add for Length<U> serves every unit you’ll ever add.

Typestate: a door that can’t be locked while open

Section titled “Typestate: a door that can’t be locked while open”

The same trick can track the state of something. A door can be open, closed or locked. You can lock a closed door, but locking an open door makes no sense. With a state label in the type, invalid moves simply don’t exist:

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// Available in every state
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// Same door, new state label
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}: {action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("opened")
}
fn lock(self) -> Door<Locked> {
self.into_state("locked")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("closed")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("unlocked")
}
}
fn main() {
let door = Door::new("server room").open().close().lock();
println!("{} is locked for the night", door.room());
let door = door.unlock().open();
println!("{} is open again", door.room());
}

Output:

server room: opened
server room: closed
server room: locked
server room is locked for the night
server room: unlocked
server room: opened
server room is open again
▶ Try it Yourself

Now try to lock an open door, Door::new("server room").open().lock():

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:48
|
50 | let door = Door::new("server room").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

Each method takes self by value, so the old door is used up and you can’t keep using a “closed” door after you opened it. The Design Patterns page shows the same typestate idea where each state also holds its own data. PhantomData is the lighter version for when the states are pure labels.

Methods for one size, and associated consts

Section titled “Methods for one size, and associated consts”

You saw impl Pair<f64> on the Generics page: methods for one concrete type only. The same works with const generics. Some methods only make sense for certain sizes:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// Only square matrices: rows and columns are the same N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// Only 2x2 matrices
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("small: trace {}, determinant {}", small.trace(), small.determinant());
println!("big: trace {}", big.trace());
}

Output:

small: trace 9, determinant -14
big: trace 15
▶ Try it Yourself

big.determinant() won’t compile, and neither will trace() on a 2x3 matrix: you get errors like error[E0599]: no method named trace found for struct Matrix<2, 3>, with the note the method was found for Matrix<N, N>. The rule “only square matrices have a trace” is now written in the types.

An associated const is a constant that belongs to a type or a trait, the way a method does. In a trait, each implementation picks its own value, and generic code can read it as U::NAME. Let’s give the units from earlier a symbol and a size:

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // how many meters one of this unit is
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "m";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "ft";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "mi";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// Works between any two units, using their constants
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

Output:

26.20 mi = 42164.81 m
29032.00 ft = 8848.95 m
29032.00 ft = 5.50 mi
▶ Try it Yourself

U::IN_METERS is looked up at compile time, so there’s no table to search at runtime. Adding a new unit, say Kilometers, is one small impl Unit block, and convert and Display work for it straight away. An inherent impl can have associated consts too, for example impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; }, read as RingBuffer::<i32, 3>::CAPACITY.

If you want one trait implementation for every type that meets a condition, that’s a blanket impl, covered on the Advanced Traits page.

These features are powerful, and that makes them tempting. Some honest advice:

  • A slice or a Vec is often enough. If a function only reads items, &[T] accepts any length and is easier to read. If the length is only known at runtime (a file, user input), you need a Vec anyway.
  • Big arrays live on the stack. A RingBuffer<u8, 10_000_000> is ten megabytes inside the struct, which can overflow the stack. Put it in a Box (see Smart Pointers) or use a Vec.
  • Every new size or type is a new copy of the code. Using a generic with 20 different values of N builds 20 versions. That costs compile time and binary size. See Performance.
  • Typestate adds types and longer error messages. It’s great for an API many people will use (a builder, a connection, a payment flow). For a state that changes based on runtime data, such as a status loaded from a database, use an enum and match.

A good rule: start simple, and reach for these tools when a real bug or a real cost shows up that they would prevent.

Doing math with a const parameter in a type. This feels natural, but stable Rust doesn’t allow it yet:

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

Fixes: use [[f32; 2]; N] (an array of pairs), add a second parameter, or use a Vec if the size doesn’t need to be in the type.

Forgetting the braces around an expression. Buffer::<2 + 2>::new() gives error: expressions must be enclosed in braces to be used as const generic arguments, and the help line shows the fix: Buffer::<{ 2 + 2 }>::new(). A plain number or a const name needs no braces.

Calling something that isn’t const in a const context. A const can’t call a normal function, and a const fn can’t use a for loop:

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

For text, use const GREETING: &str = "hello";. Inside a const fn, replace for with a while loop and an index, like the lookup table above.

#[derive(Clone, Copy)] on a PhantomData struct. You’d expect Id<User> to be Copy, since it only holds a u32. But derive adds the bound T: Copy, and User isn’t Copy, so the ID is moved instead:

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

The simplest fix is to derive Clone, Copy on the marker types too (#[derive(Clone, Copy)] struct User;). Or write the impls by hand, which needs no bound on T:

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

Write fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] that copies the scores into an array of exactly N items. Fill missing places with 0, and drop extra scores. Call it as pad_scores::<5>(&[90, 85]) and as let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);. Hint: start with [0; N], then loop over 0..N and use scores.get(i).

Show solution
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

Output:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ Try it Yourself

Here N can’t come from the argument (a slice has no length in its type), so it comes from the turbofish ::<5> or from the type you ask for, [u32; 3].

Write two const fns, minutes(n: u64) -> u64 and hours(n: u64) -> u64, that return a number of seconds. Use them to define const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); and an array const RETRY_DELAYS: [u64; 3] of 1, 5 and 15 minutes. Print both. Hint: hours can call minutes.

Show solution
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("session timeout: {SESSION_TIMEOUT} seconds");
println!("retry delays: {:?}", RETRY_DELAYS);
}

Output:

session timeout: 5400 seconds
retry delays: [60, 300, 900]
▶ Try it Yourself

The compiler works out 5400 and the delay table while building, and hours(1) + minutes(30) reads much better than a bare 5400. If you remove const from minutes, the const items stop compiling with E0015.

Make struct Euro; and struct Dollar;, and a Money<C> with a cents: i64 field and a PhantomData<C> field. Give it new(cents) and plus(self, other: Money<C>) -> Money<C>. Add a lunch and a coffee in euros and print the total. Hint: because other has the same C, a Money<Euro> and a Money<Dollar> can’t be added.

Show solution
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("total: {}.{:02} EUR", total.cents / 100, total.cents % 100);
println!("ticket: {}.{:02} USD", ticket.cents / 100, ticket.cents % 100);
}

Output:

total: 15.70 EUR
ticket: 49.99 USD
▶ Try it Yourself

Try lunch.plus(ticket): it fails with expected Money<Euro>, found Money<Dollar>. Storing cents as an integer instead of f64 also avoids rounding errors with money.

  • Const generics (const N: usize) make a number part of a type. Arrays of different lengths are different types, and impl<T, const N: usize> writes methods for every size.
  • On stable Rust, const parameters can be integers, bool or char. Expressions as arguments need braces ({ PAGE * 2 }), and math with a generic N inside a type isn’t allowed yet.
  • A const fn can run at compile time in const contexts (const, static, array lengths). Use while instead of for, and only call other const fns.
  • PhantomData<T> is a zero-size field that ties a struct to a type it doesn’t store. Use it for typed IDs, units of measure and typestate.
  • impl Matrix<2, 2> and impl<const N: usize> Matrix<N, N> give methods to some sizes only. Associated consts like U::IN_METERS give each type its own compile-time value.
  • These tools cost compile time and readability. Use them when they prevent a real bug.

Quick check

1. Why can't you pass a [u32; 4] to a function that takes [u32; 3]?

2. What does PhantomData<T> add to a struct at runtime?

3. Which of these can you NOT use inside a const fn on stable Rust?

4. What does impl<const N: usize> Matrix<N, N> mean?

Score: 0 / 4