link
Rust const fn and C++ constexpr
- Rust
const fnand C++constexprsolve a similar problem—allowing computations to happen at compile time—but their design philosophies are quite different.
1. Basic comparison
Rust const fn | C++ constexpr | |
|---|---|---|
| Main purpose | Mark a function as usable in compile-time evaluation | Mark a function/expression as potentially usable in constant evaluation |
| Compile-time call | const X: i32 = foo(); | constexpr int x = foo(); |
| Runtime call | Yes | Yes |
| Compile-time guarantee | Depends on context | Depends on context |
| Function restrictions | Relatively strict and explicit | Historically complex, but increasingly permissive |
const function syntax | const fn foo() | constexpr int foo() |
| Type-system integration | Strong | More expression-oriented |
| Compile-time errors | Very clear when required by const context | Can be subtle depending on context |
For example:
Rust
const fn square(x: i32) -> i32 {
x * x
}
const X: i32 = square(10);
fn main() {
let y = square(20); // runtime call is also allowed
}C++
constexpr int square(int x) {
return x * x;
}
constexpr int x = square(10);
int main() {
int y = square(20); // runtime call is also allowed
}Conceptually, these are very similar.
2. The important difference: Rust is more explicit
Rust’s syntax says:
const fnC++ says:
constexprBut the philosophy differs.
Rust treats const fn almost like a capability:
“This function is permitted to participate in compile-time evaluation.”
C++‘s constexpr is closer to:
“This function can be evaluated at compile time when the surrounding context permits it.”
For example:
const fn add(a: i32, b: i32) -> i32 {
a + b
}
const X: i32 = add(1, 2);The const declaration forces compile-time evaluation.
If you write:
let x = add(1, 2);Rust is free to evaluate it at runtime.
So:
const fn
│
├── compile-time context → compile time
│
└── runtime context → runtimeThis is very similar to C++.
3. Rust’s big advantage: clearer separation
Consider:
const fn fibonacci(n: u32) -> u32 {
if n <= 1 {
n
} else {
fibonacci(n - 1) + fibonacci(n - 2)
}
}
const X: u32 = fibonacci(10);The compiler knows:
X MUST be a compile-time constant
↓
fibonacci(10)
↓
const evaluationThis is particularly nice for things such as:
const BUFFER_SIZE: usize = calculate_size();If calculate_size() isn’t usable in a constant context, the compiler gives you an error rather than silently deciding to perform the calculation at runtime.
That makes const fn useful for expressing compile-time invariants.
4. C++ constexpr is more complicated
C++ has accumulated a lot of compile-time mechanisms:
constexpr
consteval
constinit
constThese have different meanings.
For example:
constexpr int square(int x) {
return x * x;
}does not mean:
“This function always executes at compile time.”
It means:
“This function is eligible for constant evaluation.”
If you want to require compile-time evaluation, modern C++ provides:
consteval int square(int x) {
return x * x;
}Now:
constexpr int x = square(10); // OKbut:
int n = 10;
int x = square(n); // errorbecause consteval requires immediate compile-time evaluation.
Rust doesn’t have an exact consteval equivalent in the same form. Instead, const contexts naturally enforce compile-time evaluation.
5. Rust’s advantage: fewer keywords
For compile-time computation, Rust generally needs:
const fn
const
staticC++ has a larger vocabulary:
constexpr
consteval
constinit
constFor someone learning the language, Rust’s model can therefore feel simpler:
const fn foo() -> usize {
42
}
const X: usize = foo();The distinction is relatively easy:
const fn→ function can be evaluated at compile timeconst→ value must be compile-time constantstatic→ statically allocated value
C++ gives you considerably more control, but consequently has more rules to understand.
6. C++‘s advantage: much more mature compile-time programming
This is where C++ is extremely powerful.
Modern C++ has gradually expanded what constexpr code can do.
For example:
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int x = factorial(10);Modern C++ allows increasingly complex operations during constant evaluation.
You can build sophisticated compile-time algorithms, manipulate arrays, use classes, and perform fairly substantial computation.
Rust also supports increasingly powerful const fn, but its compile-time evaluation rules are intentionally more conservative.
7. Rust’s biggest limitation: const fn restrictions
Not every normal Rust operation can simply be put into a const fn.
For example, you may encounter restrictions involving:
- heap allocation
- certain trait operations
- certain mutable operations
- some library APIs
- operations that aren’t
const-compatible
For example, something like:
const fn make_vec() -> Vec<i32> {
Vec::new()
}has historically been subject to limitations depending on the exact operation and Rust version.
This is because Rust’s constant evaluator must maintain strong guarantees about what can happen during compile-time evaluation.
So you can think of Rust as having:
normal Rust
│
│ restricted subset
↓
const fn
│
↓
compile-time evaluationC++ historically has had a similar concept, but its rules have become much more permissive over successive standards.
8. C++ advantage: constexpr works beautifully with templates
This is one of C++‘s biggest strengths.
For example:
template <int N>
constexpr int square() {
return N * N;
}
constexpr int x = square<10>();Compile-time programming is deeply integrated with:
- templates
- type traits
- concepts
constexprconsteval- parameter packs
This makes C++ extremely powerful for metaprogramming.
Rust has generics and const generics:
fn foo<const N: usize>() {
// ...
}and:
const fn square(x: usize) -> usize {
x * x
}but Rust deliberately avoids reproducing C++‘s template-metaprogramming model.
That’s generally a complexity advantage for Rust, but sometimes a power advantage for C++.
9. Rust’s const fn is particularly nice with const generics
This is a very useful Rust combination:
const fn buffer_size(width: usize, height: usize) -> usize {
width * height * 4
}
struct Image<const N: usize> {
data: [u8; N],
}
type Image1920x1080 = Image<{
buffer_size(1920, 1080)
}>;The calculation can participate directly in the type-level constant.
Conceptually:
buffer_size(1920, 1080)
↓
8,294,400
↓
const generic
↓
Image<8_294_400>This is one of the nicest aspects of Rust’s approach.
10. Error handling is another major difference
Rust’s type system makes compile-time restrictions very explicit.
For example:
const fn divide(a: usize, b: usize) -> usize {
a / b
}
const X: usize = divide(10, 0);The compiler must evaluate:
divide(10, 0)and therefore detects the invalid operation during compilation.
That’s desirable because the const declaration explicitly demands a compile-time value.
C++ also performs constant evaluation and rejects invalid constant expressions:
constexpr int divide(int a, int b) {
return a / b;
}
constexpr int x = divide(10, 0);But C++‘s broader constant-expression machinery can sometimes make the rules harder to reason about.
11. Rust’s philosophy: “const correctness” is stronger
A useful way to think about it is:
C++
constexpr
↓
"this can potentially be evaluated at compile time"Rust
const fn
↓
"this function is allowed to participate in const evaluation"and:
const X = ...
↓
"this particular expression MUST be evaluated as a constant"So Rust separates permission and requirement fairly cleanly.
12. Performance
An important point:
Neither const fn nor constexpr automatically makes runtime calls faster.
For example:
const fn square(x: i32) -> i32 {
x * x
}
fn main() {
let x = square(100);
}The fact that square is const fn does not mean every call must be compile-time evaluated.
Likewise:
constexpr int square(int x) {
return x * x;
}doesn’t force runtime calls to become compile-time calls.
The compiler may inline and optimize either way.
The real benefit is:
compile-time known input
↓
compile-time computation
↓
no runtime computation required13. Pros and cons
Rust const fn
Pros
- Simple conceptual model
- Explicit compile-time capability
- Excellent integration with
constand const generics - Strong compiler enforcement
- Less template-metaprogramming complexity
- Good readability
- Compile-time invariants are easy to express
Cons
- More restrictions on what can be done inside
const fn - Some standard-library APIs aren’t usable in const contexts
- Compile-time programming is less expressive than C++‘s metaprogramming ecosystem
- Constant evaluation features are still evolving
C++ constexpr
Pros
- Extremely powerful
- Very mature compile-time programming ecosystem
- Excellent integration with templates
- Increasingly permissive in modern C++
constevalcan explicitly require compile-time evaluation- Can express sophisticated compile-time algorithms
Cons
- More complicated rules
- Multiple overlapping keywords (
const,constexpr,consteval,constinit) - Template +
constexprmetaprogramming can become difficult to read - Compile-time/runtime boundaries can be less obvious
- More language rules to understand
14. A good mental model
I’d summarize the difference like this:
COMPILE-TIME PROGRAMMING
C++ Rust
────────────────── ──────────────────
constexpr const fn
│ │
├── can run at compile ├── can run at compile
└── can run at runtime └── can run at runtime
consteval const context
│ │
└── MUST compile └── MUST compileThe biggest philosophical difference is:
C++ gives you a very powerful and flexible compile-time programming system. Rust gives you a more constrained, explicit, and type-system-oriented one.
For ordinary compile-time calculations, I prefer Rust’s model because it is easier to reason about.
For advanced compile-time metaprogramming, C++ is still more powerful, largely because constexpr is deeply integrated with the rest of C++‘s template system.