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!