constexpr and const fn: C++ and Rust
Both languages let the compiler compute a table that the program then reads for free. Three things decide how that feels in practice: which functions may take part, how an assertion about the result is written, and whether the computation is allowed to allocate. The two programs below build the same ten Fibonacci numbers and check the last one before the program exists.
The task in C++
A constexpr function takes part in a constant expression when its arguments are constant, and it stays callable at run time. The array and its type are built by make_table, which the compiler evaluates; static_assert then checks a property of the result during the build.
The table is a constexpr variable at namespace scope, so it is part of the binary and every use reads the same ten numbers.
#include <array>
#include <iostream>
constexpr unsigned fib(unsigned n)
{
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
constexpr std::size_t count = 10;
constexpr std::array<unsigned, count> make_table()
{
std::array<unsigned, count> table{};
for (std::size_t i = 0; i < count; ++i) {
table[i] = fib(static_cast<unsigned>(i));
}
return table;
}
constexpr auto table = make_table();
static_assert(table[9] == 34, "the tenth Fibonacci number is 34");
int main()
{
for (std::size_t i = 0; i < count; ++i) {
std::cout << (i ? " " : "") << table[i];
}
std::cout << '\n' << table[9] << ' ' << table.size() << '\n';
}
g++ -std=c++23 -Wall -Wextra table.cpp -o table
./table
0 1 1 2 3 5 8 13 21 34
34 10
The loop does not run at run time. The array is a constant object and the program contains its bytes, which is the same shape as a lookup table written by hand and a great deal less error-prone. The C++ lesson for this ground is constexpr functions.
The same task in Rust
The function carries const fn, the array type carries its length, and the table is a static initialised by a call. The assertion is an unnamed constant whose initialiser panics, which the evaluator turns into a build error.
The loop inside make_table is a while with an index, because a for over a range is not allowed in a constant expression.
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 table.rs
./table
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
34 10
The numbers are the same and the debug line is the Rust way of printing a whole array. The Rust lesson for this ground is const fn and compile-time work.
What differs and why
Which functions take part: in C++ a function is usable in a constant expression when it is marked constexpr and its body satisfies the rules of the standard; a template or a lambda may also be evaluated there. In Rust the marker is const fn and the evaluator is a separate interpreter with its own list of allowed operations, which is narrower than the language and grows release by release.
What the assertion looks like: static_assert is a built-in construct with a message. Rust has no such keyword, so the check is an ordinary const item of type () whose initialiser calls assert!; a failed check stops the build with a panic from the evaluator. The second program of this page shows the mechanism.
Who checks the function: a C++ constexpr function is checked when it is evaluated, so a body that breaks a rule may compile until some constant expression calls it. A Rust const fn is checked at its definition, which is why the error below points at the call inside the function rather than at the constant that used it.
What may happen at compile time: C++ 20 allows std::string and std::vector in a constant expression as long as every allocation is freed before the evaluation ends. Rust allows arrays and slices and no heap at all, so the same habit fails.
The habit that breaks
The habit is to count digits at compile time by building a string. It compiles and runs in C++ 20 and later, where constexpr evaluation owns a temporary std::string.
#include <iostream>
#include <string>
constexpr std::size_t digits(unsigned n)
{
std::string text;
do {
text.push_back(static_cast<char>('0' + n % 10));
n /= 10;
} while (n != 0);
return text.size();
}
constexpr std::size_t len = digits(2024);
static_assert(len == 4, "2024 has four digits");
int main()
{
std::cout << len << '\n';
}
g++ -std=c++23 -Wall -Wextra digits.cpp -o digits
./digits
4
The same function in Rust with a String stops at the push. The compiler reports the non-constant method and then the destructor that the evaluator cannot run.
const fn digits(n: u32) -> usize {
let mut text = String::new();
let mut value = n;
while value != 0 {
text.push('0');
value /= 10;
}
text.len()
}
const LEN: usize = digits(2024);
fn main() {
println!("{LEN}");
}
rustc --edition 2024 digits.rs
error[E0015]: cannot call non-const method `String::push` in constant functions
--> digits.rs:5:14
|
5 | text.push('0');
| ^^^^^^^^^^^
|
= 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
--> digits.rs:2:9
|
2 | let mut text = String::new();
| ^^^^^^^^ the destructor for this type cannot be evaluated in constant functions
...
9 | }
| - value is dropped here
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0015, E0493.
For more information about an error, try `rustc --explain E0015`.
The Rust version counts with arithmetic instead: a while that divides by ten and increments a counter, which the evaluator runs without a heap. The C++ lesson for this ground is constexpr functions, and the Rust lesson is const fn and compile-time work. The previous page in this set is Operator overloading.
The C++ programs were built with g++ 15.2.0 and the Rust programs with rustc 1.96.1, and both were tested on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev