const fn and compile-time work

A constant is a value the compiler computes while it builds the program. Rust marks the functions that may take part in that with const fn, and a whole family of ordinary code becomes available at compile time: arithmetic, comparisons, loops, slices and panics. The result is a table that costs nothing at start-up and an error that appears before the program runs.

Coming from C++? constexpr and const fn builds the same table with both compilers and shows what one language accepts and the other refuses.

This lesson writes a const fn, uses it in a constant, sizes an array with another one, puts a loop and a slice into a constant expression and asserts a property of the table at build time. The last section is the allocation that the compiler will not run.

A const fn

The keyword goes before fn and promises that the body is limited to operations the evaluator can perform. Such a function is still an ordinary function: the program may call it at run time with a value it only sees then, and the compiler may call it earlier with a value it already knows.

The program computes a square three ways: through a constant, through another constant built from the first, and through a call in main. All three go through the same function.

const fn square(n: u32) -> u32 { n * n } const SIDE: u32 = 12; const AREA: u32 = square(SIDE); fn main() { println!("{} {} {}", SIDE, AREA, square(5)); }

rustc --edition 2024 c1.rs
./c1

12 144 25

AREA is a number in the binary, not a call in the start-up path. A function without the keyword could not be used in the third line, and the error would name the call. The lesson on const and static covers the two items themselves; this one is about the functions that fill them.

A constant that sizes an array

An array type carries its length, so the length has to be known before the program runs. A const fn can compute it from the other constants, and a static can hold a table whose type uses that length.

The program asks whether a year is a leap year and how many days it has. Both answers come from functions that run at compile time when the argument is a constant, and at run time when it is not.

const fn is_leap(year: u32) -> bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } const fn days(year: u32) -> u32 { if is_leap(year) { 366 } else { 365 } } const DAYS_2024: u32 = days(2024); static MONTHS: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; fn main() { println!("{} {} {}", DAYS_2024, MONTHS.len(), is_leap(2023)); println!("{}", days(1900)); }

rustc --edition 2024 c2.rs
./c2

366 12 false
365

The first line is computed by the compiler and printed at run time. The second line shows the rule for a century year: 1900 is divisible by four and by a hundred but not by four hundred, so it has 365 days, which is the case a hand-written test usually forgets.

A const is inlined at every use and has no address, while a static is one value with one address that every use shares. A table of twelve numbers is small enough for either, and a table of a thousand rows is worth a static so that it exists once in the binary.

A loop in a constant expression

The evaluator runs while and loop, reads slices by index, and stops a runaway loop with a limit of its own rather than hanging the build. That is enough for a factorial and for the sum of a list, which are the two shapes most compile-time tables need.

The program computes ten factorial and zero factorial as constants, and sums a slice constant, then calls the same functions with values that only exist at run time.

const fn factorial(n: u64) -> u64 { let mut result = 1; let mut i = 2; while i <= n { result *= i; i += 1; } result } const fn sum(values: &[u32]) -> u32 { let mut total = 0; let mut i = 0; while i < values.len() { total += values[i]; i += 1; } total } const TEN: u64 = factorial(10); const TOTAL: u32 = sum(&[1, 2, 3, 4]); fn main() { println!("{} {} {} {}", TEN, factorial(0), TOTAL, sum(&[10, 20])); }

rustc --edition 2024 c3.rs
./c3

3628800 1 10 30

The zero case is the one a loop gets wrong, and here it returns one because the body never runs. The variadic-looking sum(&[10, 20]) at run time is an argument of main, so the compiler does not know it in advance, and the same code runs in the binary. The lesson on slices explains why a slice is the parameter type for a list that the caller may own in several ways.

A static table and its assertion

A table of Fibonacci numbers is a good compile-time job: the numbers are small, the recursion is quick and the size is a constant. The assertion above it is the replacement for static_assert in C++: a const item of type () whose initializer panics, which turns a broken assumption into a build error.

The program builds ten numbers in a const fn, checks that they fit in a u32, and prints the table and one element of it.

const fn fib(n: usize) -> u32 { if n < 2 { n as u32 } else { fib(n - 1) + fib(n - 2) } } const COUNT: usize = 10; const _: () = assert!(COUNT <= 40, "the table is too large for u32"); static FIB: [u32; COUNT] = make_table(); const fn make_table() -> [u32; COUNT] { let mut table = [0; COUNT]; let mut i = 0; while i < COUNT { table[i] = fib(i); i += 1; } table } fn main() { println!("{:?}", FIB); println!("{} {}", FIB[9], FIB.len()); }

rustc --edition 2024 c4.rs
./c4

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
34 10

The array is in the binary as ten numbers in a row, and the program reads it without any computation. The recursion inside make_table is evaluated once, by the compiler; a fib that allocated a cache or read a clock could not be part of it, which is the subject of the next section.

Change COUNT to forty-one and the build stops with the message from the assertion, before any code runs and before a test could forget the case. The lesson on assertions covers the same macros at run time, and the lesson on arrays explains the [u32; N] type whose length is a constant.

The allocation in a const fn

The evaluator has no heap, no operating system and no clock. A const fn that calls to_string asks for an allocation, and the compiler reports two errors: the call is not a constant operation, and the String it returns has a destructor that cannot run at compile time.

const fn double(n: u32) -> u32 { n.to_string().len() as u32 } fn main() { println!("{}", double(21)); }

rustc --edition 2024 c5b.rs

error[E0015]: cannot call non-const method `<u32 as ToString>::to_string` in constant functions
--> c5b.rs:2:7
|
2 | n.to_string().len() as u32
| ^^^^^^^^^^^
|
note: method `to_string` is not const because trait `ToString` is not const
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/alloc/src/string.rs:2866:0
|
= note: this trait is not const
::: /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/alloc/src/string.rs:2880:4
|
= note: this method is not const
= help: const traits are not yet supported on stable Rust
= note: calls in constant functions are limited to constant functions, tuple structs and tuple variants

error[E0493]: destructor of `String` cannot be evaluated at compile-time
--> c5b.rs:2:5
|
2 | n.to_string().len() as u32
| ^^^^^^^^^^^^^^^^ - temporary is part of expression evaluated at compile-time
|
= note: the destructor was found to be called on this type
= help: consider using `std::mem::ManuallyDrop` instead

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0015`.

The first error is the real one and the second is its consequence: even if the call returned a value, the temporary would have to be dropped. The fix is to count the digits with arithmetic, which the evaluator can do, or to leave the function as an ordinary one and call it at run time. The next lesson turns to the other half of a compile-time decision: what a library shows and what it keeps to itself.

Exercises

  1. Write a const fn that returns the number of decimal digits of a u32 without allocating, and use it to size an array.
  2. Build a static table of the first twelve powers of two and assert at compile time that the last one fits in a u32.
  3. Change COUNT to forty-one and read the message the compiler prints.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is a const fn?

    Answer

    A function that may be evaluated while the program is built and also called at run time

    The keyword widens where the function may run; it stays callable with a run-time value.

  2. What does const _: () = assert!(N < 10); do?

    Answer

    Checks the condition while the program is compiled

    The item has no name and its initializer panics in the evaluator, so the build fails.

  3. What can a const fn not do?

    Answer

    Allocate on the heap

    The evaluator has no heap, so allocation and destructors are refused.

  4. What does a static give that a const does not?

    Answer

    One value with one fixed address, shared by every use

    A const is copied into each use, while a static exists once in the binary.

  5. Why does an array length have to be a constant?

    Answer

    The length is part of the type, which the compiler fixes before the program runs

    [T; N] carries N in its type, so N must be known while compiling.

  6. What does this program print? const fn factorial(n: u64) -> u64 { let mut result = 1; let mut i = 2; while i <= n { result *= i; i += 1; } result } const SIX: u64 = factorial(6); fn main() { println!("{} {}", SIX, factorial(3)); }

    Answer

    720 6

    Six factorial is 720 and three factorial is 6, one from the compiler and one at run time.

  7. What does this program print? const fn double(n: u32) -> u32 { n * 2 } const N: u32 = 21; const VALUE: u32 = double(N); fn main() { println!("{} {}", VALUE, double(N + 1)); }

    Answer

    42 44

    The constant is 42, and the call in main doubles 22 at run time.

  8. What does this program print? const fn sum(values: &[u32]) -> u32 { let mut total = 0; let mut i = 0; while i < values.len() { total += values[i]; i += 1; } total } const TOTAL: u32 = sum(&[4, 5, 6]); fn main() { println!("{} {}", TOTAL, sum(&[1, 1])); }

    Answer

    15 2

    A slice of a constant array is a constant value, and both sums are correct.

  9. What is the first line of the compiler output for this program? fn size() -> usize { 3 } fn main() { let data = [0u8; size()]; println!("{}", data.len()); }

    Answer

    error[E0015]: cannot call non-const function size in constants

    The length needs a constant expression, and a plain function is not one.

  10. What is the first line of the compiler output for this program? const LEN: usize = String::new().len(); fn main() { println!("{LEN}"); }

    Answer

    error[E0493]: destructor of String cannot be evaluated at compile-time

    A String has a destructor, and the evaluator cannot run it, so the constant is refused.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev