constexpr
¶
Compilers Always Did Runtime Work¶
Even back in the old C days, a compiler knew how to add two values
Compiler calculates
40+2
, and initializesanswer
with that constant
#include <iostream>
int main()
{
int answer = 40+2; // <-- addition at compiletime
std::cout << answer << '\n';
return 0;
}
⟶ Continue on Godbolt compiler explorer
Motivation: Let Compiler Do More Of This¶
Extract
40+2
into a function⟶ this turns into a function call, rather than a precalculated constant
Would be cool if not
⟶ binary size reduction, startup time
#include <iostream>
int add(int l, int r)
{
return l+r;
}
int main()
{
const int a=40, b=2; // <-- immutable!
int answer = add(a, b);
std::cout << answer << '\n';
return 0;
}
Even -O3
does not make it go away …
$ g++ -O3 constexpr-add-simple-function.cpp
$ nm --demangle a.out |grep add
00000000004011a0 T add(int, int)
And static
? Why Not Use static
?¶
static
functions are only visible inside their own compilation unit⟶ compiler has complete freedom to do whatever with it
#include <iostream>
static int add(int l, int r) // <-- only visible inside this compilation unit
{
return l+r;
}
int main()
{
const int a=40, b=2;
int answer = add(a, b);
std::cout << answer << '\n';
return 0;
}
Optimization off ⟶ function call
$ g++ -O0 constexpr-add-simple-function-static.cpp
$ nm --demangle a.out |grep add
0000000000401136 t add(int, int)
-O3
⟶ compiler proves that eliminatingadd()
has no side effects (no outside calls possible) - and eliminates it
$ g++ -O3 constexpr-add-simple-function-static.cpp
$ nm --demangle a.out |grep add
Note
This is not guaranteed! (Results produced with GCC 13)
Enter constexpr
¶
Historically, compilers always did such inlining and/or precalculations
Not only for trivial functions as
40+2
, but for varying degrees of complexityNontrivial machinery: think about cross compilation, for example
It’s just that one cannot count on those optimizations
They’re gone with
-O0
⟶ debug and release completely different⟶
constexpr
constexpr
Functions: Evaluated At Runtime OR Compiletime¶
#include <iostream>
constexpr int add(int l, int r)
{
return l+r;
}
int main()
{
const int a=40, b=2;
int answer = add(a, b);
std::cout << answer << '\n';
return 0;
}
$ g++ -O0 constexpr-add-constexpr-function.cpp
$ nm --demangle a.out |grep add
Again, this is not guaranteed!
-
x86-64 GCC 15: evaluates in
constexpr
contextx86-64 clang 14: generates a call, even though the parameters are immutable
⟶ not much better than
static
constexpr
impliesinline
though
Forcing Compiletime Evaluation¶
#include <iostream>
constexpr int add(int l, int r)
{
return l+r;
}
int main()
{
const int a=40, b=2; // <-- immutable
constexpr int answer = add(a, b); // <-- force compiletime evaluation
std::cout << answer << '\n';
return 0;
}
This is necessary! 🤔
consteval fixes this
So What Are The Rules? constexpr
Functions Are Dual¶
Rules
If the user wants compiletime evaluation, then they have to say so
Only immutable parameters!
Runtime evaluation has no constraints - function is just called
#include <iostream>
constexpr int add(int l, int r)
{
return l+r;
}
int main()
{
const int a=40, b=2; // <-- immutable
constexpr int answer = add(a, b); // <-- force compiletime evaluation
std::cout << answer << '\n';
return 0;
}
If the user wants to supply mutable parameters, they do not request compiletime evaluation
#include <iostream>
constexpr int add(int l, int r)
{
return l+r;
}
int main()
{
int a=42, b=2; // <-- mutable
int answer = add(a, b); // <-- *don't* force compiletime evaluation
std::cout << answer << '\n';
return 0;
}
Uncool: Forcing Compiletime Evaluation Leads To constexpr
Result¶
One has to force
constexpr
context, even when calling with immutable parameters (see here)Assigned-to variable has to be
constexpr
⟶ this sucks somehow!
#include <iostream>
constexpr int add(int l, int r)
{
return l+r;
}
int main()
{
const int a=40, b=1;
constexpr int answer = add(a, b);
++answer; // error: increment of read-only variable ‘answer’
std::cout << answer << '\n';
return 0;
}
constexpr
Callchains¶
When a
constexpr
function is evaluated inconstexpr
context, all its calls have to beconstexpr
#include <iostream>
constexpr int add(int l, int r)
{
return l+r;
}
constexpr int add3(int v1, int v2, int v3)
{
return add(v1, add(v2, v3));
}
int main()
{
const int a=40, b=2, c=624;
constexpr int answer = add3(a, b, c);
std::cout << answer << '\n';
return 0;
}
Recursion¶
#include <iostream>
constexpr int add(int l, int r)
{
return l+r;
}
constexpr int sum(int num)
{
if (num == 1)
return num;
else
return add(sum(num-1), num);
}
int main()
{
const int n = 3;
constexpr int answer = sum(n);
std::cout << answer << '\n';
return 0;
}
constexpr
Classes¶
#include <cmath>
#include <iostream>
class point
{
public:
constexpr point(int x, int y) : _x(x), _y(y) {}
constexpr double abs() const
{
return std::sqrt(_x*_x + _y*_y); // <-- constexpr
}
private:
int _x;
int _y;
};
int main()
{
constexpr point p(3, 4);
constexpr auto abs = p.abs();
std::cout << abs << '\n';
// std::cout << p.abs() << '\n'; // defeats constexpr somewhat
return 0;
}
constexpr
Algorithms, constexpr std::vector
¶
Rather new: C++20
GCC 15 (newest, as of 2025-05-08) does not support this
⟶ apparently,
constexpr
dynamic allocations are not trivial
#include <algorithm>
#include <vector>
#include <iostream>
int main()
{
constexpr std::vector numbers{2, 1, 3};
constexpr std::sort(numbers.begin(), numbers.end());
for (auto n: numbers)
std::cout << n << std::endl;
return 0;
}
Afterword: Cool¶
Compiler has a “virtual machine” of some sort that evaluates C
When cross-compiling, that machine’s architecture is the target architecture
⟶ This is not trivial!