constexpr and consteval functions
Intro
In C++23, we have three kinds of functions:
- Runtime functions
constexpr
functionsconsteval
functions
They can be demonstrated as follows:
// This is a pure compile-time function.
// Any evaluation is fully done at compile-time;
// no runtime code will be generated by the compiler, just like `static_assert`.
consteval size_t strlen_ct(const char* s) {
size_t n = 0;
for (; s[n] != '\0'; ++n);
return n;
}
// This is a pure runtime function, which can only be invoked at runtime.
size_t strlen(const char* s);
// This function can be invoked both at both compile-time and at runtime,
// depending on the context.
constexpr size_t strlen_dual(const char* s) {
if consteval {
return strlen_ct(s); // compile-time path
} else {
return strlen(s); // runtime path
}
}
Let’s take it to the next level. How do the three kinds of functions interact with each other?
Who Can Call Which
Here “call” may mean two things:
- Write code to call
- Actual evaluation (during compile time)
Can constexpr
functions call runtime functions?
The answer is:
- You can write code to call runtime functions
- The code must not be reached during compile-time evaluation
constexpr int divide(int a, int b) {
if (b == 0) {
printf("Divide by zero\n");
return 0;
} else {
return a / b;
}
}
static_assert( divide(6, 3) == 2 ); // Ok
static_assert( divide(0, 0) == 0 ); // Error: call to runtime function `printf`
This is akin to the treatment of exceptions in constexpr
functions;
you can write throw
, you just can’t catch
- any throw
reached will trigger an error.
C++23, specifically P2448, allows you to even write:
// runtime function
void g();
// Ok since C++23
constexpr int f() {
g();
return 0;
}
The above code compiles, even though any compile-time invocation of f
will cause an error:
static_assert( f() == 0 ); // Error: call to runtime function `g`
Ordinary invocation is fine:
int n = f(); // Ok
Such functions like f
are a bit deceiving:
despite the constexpr
specifier, they can’t be evaluated at compile time.
With this example in mind, a constexpr
function:
- may be available for compile-time evaluation, but no guarantee that it can
Can consteval
functions call runtime functions?
At first, it may seem the answer is “no”. consteval
functions only evaluate at compile time,
so any call to runtime function must be an error, right?
But C++ is actually quite permissive here:
just like constexpr
functions, you can write code to call runtime functions,
so long as you don’t actually reach that code during compile-time evaluation.
// runtime function
int g();
// Ok, even though `f` can never be called
consteval int f() {
return g();
}
So far, the only rule to remember is:
- You cannot evaluate runtime functions at compile-time
Can runtime functions call constexpr
functions?
Mostly, yes. In fact, most of the functions in the standard library are now constexpr
; it would be really bad if users’ runtime functions can’t call them!
constexpr size_t strlen_dual(const char* s) {
if consteval {
return strlen_ct(s); // compile-time path
} else {
return strlen(s); // runtime path
}
}
// runtime function
int main(int argc, char** argv) {
int n1 = strlen_dual(argv[0]); // Ok, takes the runtime path,
// because argv[0] is a runtime value
}
Can consteval
functions call constexpr
functions?
Yes. Intuitively:
constexpr
functions support (at least advertise to) compile-time evaluationconsteval
functions must evaluate at compile-time
So consteval
functions just take the compile-time path for the constexpr
functions they call.
constexpr bool is_constant_evaluated() {
if consteval {
return true; // compile-time path
} else {
return false; // runtime path
}
}
consteval bool f() {
return is_constant_evaluated(); // true
}
Can runtime functions call consteval
functions?
Yes, but only if all arguments are constant expressions.
The evaluation is immediate and done at compile-time:
consteval size_t strlen_ct(const char* s) {
...
}
int main() {
return strlen_ct(""); // same as return 0
}
Can constexpr
functions call consteval
functions?
This is the interesting part.
consteval
functions can always be invoked if all the arguments are constant expressions, thus code like strlen_ct("")
works in constexpr
functions too.
There are two things that separate constexpr
functions from runtime functions,
both of which have to do with passing parameters down to consteval
functions.
if consteval
Firstly, inside the if consteval
block,
constexpr
functions’ formal parameters can be used as arguments for consteval
functions.
This is why our first example works:
consteval size_t strlen_ct(const char* s) {
...
}
constexpr size_t strlen_dual(const char* s) {
if consteval {
// `s` is promoted to be usable as arguments for consteval functions
return strlen_ct(s);
} else {
return strlen(s);
}
}
Importantly, if consteval
doesn’t turn function parameters into a full-fledged constant expressions:
constexpr int identity(int a) {
if consteval {
// Doesn't work: error: 'a' is not a constant expression
static_assert( a == a );
return a;
} else {
return a;
}
}
Closely related is the parameters of consteval
functions:
consteval int identity_ct(int a) {
// Doesn't work either: error: 'a' is not a constant expression
static_assert( a == a );
return a;
}
I find this a bit lacking. Think about it:
- At call site, the arguments for
consteval
functions must be constant expressions - Inside the
consteval
function body, the parameters are not constant expressions
In short, the very same values lose their constant-expression-ness at the consteval
function boundary.
And apparently, I’m not the only one with this thought. See this Stackoverflow question.
The example provided is:
consteval auto foo(int i) {
return std::integral_constant<int, i>();
}
If i
is allowed to be a constant expression,
then foo
would return different types for different values of i
.
Functions can’t do that (any function has exactly one return type); only function templates can.
Therefore, some language changes are needed here for this to work.
Either foo
is implictly turned into a function template,
or we need something like P1045 - constexpr Function Parameters
Anyway, what we have right now is:
- Function parameters are not constant expression
- At best, they can be used as arguments to call
consteval
function insideif consteval
block
Escalation
The second thing is,
if a constexpr
function template, outside an if consteval
, calls a consteval
function with some non-constant-expression arguments, then it becomes consteval
:
consteval int identity_ct(int x) {
return x;
}
template <class T>
constexpr T identity(T x) {
// not in a `if consteval` block,
// calls consteval `identity_ct` with `x` (not a constant expression),
// which triggers "escalation",
// which makes `identity<T>` consteval
return identity_ct(x);
}
static_assert( identity(0) == 0 ); // Ok
int main(int argc, char** argv) {
// Error: `identity` can no longer be invoked ordinarily (with runtime values)
return identity(argc);
}
Note that consteval
escalation works in a few other cases, but free function is not one of them:
// still an error; `identity` does not become consteval
constexpr int identity(int x) {
return identity_ct(x);
}
The full mechanic and rationale is described in this paper, P2564 - consteval needs to propagate up. Note that while this is a C++23 language feature, only the very recent compilers (e.g., GCC 14+, Clang 17+) support it.
Some thoughts
So we have “bad” constexpr
functions that are runtime only:
// runtime function
int zero();
constexpr int f(int) {
return zero();
}
int main(int argc, char**) {
f(argc); // Ok
static_assert(f(0) == 0); // Error
}
and the other kind of “bad” constexpr
functions that are compile-time only:
// compile-time function
consteval int zero() { return 0; }
template <class T>
constexpr T f(T) {
return zero();
}
int main(int argc, char**) {
f(argc); // Error
static_assert(f(0) == 0); // Ok
}
and of course the good kind that are both runtime and compile-time friendly,
which (fortuanately) all the constexpr
functions in the standard library are.
Perhaps it is a good practice not to write “bad” constexpr
functions;
if you mark your functions constexpr
, make sure it is invocable at both compile-time and runtime.