Intro

In C++23, we have three kinds of functions:

  1. Runtime functions
  2. constexpr functions
  3. consteval 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:

  1. Write code to call
  2. 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 evaluation
  • consteval 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 inside if 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.