Constexpr Functions Part 3 and Consteval

Forcing a Constexpr Function to Be Evaluated at Compile-Time

There's no way to tell the compiler that a constexpr function should prefer to evaluate at compile-time whenever it can (e.g., in cases where the return value of a constexpr function is used in a non-constant expression).

However, we can force a constexpr function that's eligible to be evaluated at compile-time to actually evaluate at compile-time by ensuring the return value is used where a constant expression is required. This needs to be done on a per-call basis.

The most common way to do this is to use the return value to initialize a constexpr variable (this is why we've been using variable 'max1' in prior examples). Unfortunately, this requires introducing a new variable into our program just to ensure compile-time evaluation, which is ugly and reduces code readability.

For advanced readers: There are several hacky ways that people have tried to work around the problem of having to introduce a new constexpr variable each time we want to force compile-time evaluation. See various blog posts on this topic.

However, in C++20, there's a better workaround to this issue, which we'll present in a moment.

Consteval C++20

C++20 introduces the keyword consteval, which is used to indicate that a function must evaluate at compile-time, otherwise a compile error will result. Such functions are called immediate functions.

#include <iostream>

consteval int maximum(int p, int q) // function is now consteval
{
    return (p > q ? p : q);
}

int main()
{
    constexpr int max1 { maximum(10, 15) }; // OK: evaluates at compile-time
    std::cout << max1 << '\n';

    std::cout << maximum(10, 15) << " is larger!\n"; // OK: evaluates at compile-time

    int a{ 10 }; // not constexpr
    std::cout << maximum(a, 15) << " is larger!\n"; // error: consteval functions must evaluate at compile-time

    return 0;
}

In the above example, the first two calls to maximum() will evaluate at compile-time. The call to maximum(a, 15) cannot be evaluated at compile-time, so a compile error results.

Best practice: Use consteval if you have a function that must evaluate at compile-time for some reason (e.g., because it does something that can only be done at compile time).

Perhaps surprisingly, the parameters of a consteval function are not constexpr (even though consteval functions can only be evaluated at compile-time). This decision was made for the sake of consistency.

Determining if a Constexpr Function Call Is Evaluating at Compile-Time or Runtime

C++ doesn't currently provide any reliable mechanisms to do this.

What About std::is_constant_evaluated or if consteval? Advanced

Neither of these capabilities tell you whether a function call is evaluating at compile-time or runtime.

std::is_constant_evaluated() (defined in the <type_traits> header) returns a bool indicating whether the current function is executing in a constant-evaluated context. A constant-evaluated context (also called a constant context) is defined as one in which a constant expression is required (such as the initialization of a constexpr variable). So in cases where the compiler is required to evaluate a constant expression at compile-time std::is_constant_evaluated() will return true as expected.

This is intended to allow you to do something like this:

#include <type_traits>

constexpr int processData()
{
    if (std::is_constant_evaluated()) // if evaluating in constant context
        performCompileTimeAction();
    else
        performRuntimeAction();
}

However, the compiler may also choose to evaluate a constexpr function at compile-time in a context that doesn't require a constant expression. In such cases, std::is_constant_evaluated() will return false even though the function did evaluate at compile-time. So std::is_constant_evaluated() really means "the compiler is being forced to evaluate this at compile-time", not "this is evaluating at compile-time".

While this may seem strange, there are several reasons for this:

  1. As the paper that proposed this feature indicates, the standard doesn't actually make a distinction between "compile time" and "runtime". Defining behavior involving that distinction would have been a larger change.
  2. Optimizations should not change the observable behavior of a program (unless explicitly allowed by the standard). If std::is_constant_evaluated() were to return true when the function was evaluated at compile-time for any reason, then the optimizer deciding to evaluate a function at compile-time instead of runtime could potentially change the function's observable behavior. As a result, your program might behave very differently depending on what optimization level it was compiled with!

While this could be addressed in various ways, those involve adding additional complexity to the optimizer and/or limiting its ability to optimize certain cases.

Introduced in C++23, if consteval is a replacement for if (std::is_constant_evaluated()) that provides a nicer syntax and fixes some other issues. However, it evaluates the same way.

Using Consteval to Make Constexpr Execute at Compile-Time C++20

The downside of consteval functions is that such functions can't evaluate at runtime, making them less flexible than constexpr functions, which can do either. Therefore, it would still be useful to have a convenient way to force constexpr functions to evaluate at compile-time (even when the return value is being used where a constant expression isn't required), so we can explicitly force compile-time evaluation when possible, and runtime evaluation when we can't.

Here's an example showing how this is possible:

#include <iostream>

#define FORCE_COMPILE_TIME(...) [] consteval { return __VA_ARGS__; }() // C++20 version
#define FORCE_COMPILE_TIME_11(...) [] { constexpr auto _ = __VA_ARGS__; return _; }() // C++11 version

// This function returns the larger of two numbers if executing in a constant context
// and the smaller of two numbers otherwise
constexpr int compare(int p, int q) // function is constexpr
{
    if (std::is_constant_evaluated())
        return (p > q ? p : q);
    else
        return (p < q ? p : q);
}

int main()
{
    int a { 10 };
    std::cout << compare(a, 15) << '\n'; // executes at runtime and returns 10

    std::cout << compare(10, 15) << '\n'; // may or may not execute at compile-time, but will always return 10
    std::cout << FORCE_COMPILE_TIME(compare(10, 15)) << '\n'; // always executes at compile-time and returns 15

    return 0;
}

For advanced readers: This uses a variadic preprocessor macro (the #define, ..., and __VA_ARGS__) to define a consteval lambda that's immediately invoked (by the trailing parentheses). You can find information on variadic macros at cppreference.com. We cover lambdas in a later lesson on lambdas.

The following should also work (and is a bit cleaner since it doesn't use preprocessor macros):

For gcc users: There's a bug in GCC 14 onward that causes the following example to produce the wrong answer when any level of optimization is enabled.

#include <iostream>

// Uses abbreviated function template (C++20) and `auto` return type to make this function work with any type of value
// See 'related content' box below for more info (you don't need to know how these work to use this function)
// We've opted to use an uppercase name here for consistency with the prior example, but it also makes it easier to see the call
consteval auto FORCE_COMPILE_TIME(auto value)
{
    return value;
}

// This function returns the larger of two numbers if executing in a constant context
// and the smaller of two numbers otherwise
constexpr int compare(int p, int q) // function is constexpr
{
    if (std::is_constant_evaluated())
        return (p > q ? p : q);
    else
        return (p < q ? p : q);
}

int main()
{
    std::cout << FORCE_COMPILE_TIME(compare(10, 15)) << '\n'; // executes at compile-time

    return 0;
}

Because the arguments of consteval functions are always manifestly constant evaluated, if we call a constexpr function as an argument to a consteval function, that constexpr function must be evaluated at compile-time! The consteval function then returns the result of the constexpr function as its own return value, so the caller can use it.

Note that the consteval function returns by value. While this might be inefficient to do at runtime (if the value was some type that's expensive to copy, e.g., std::string), in a compile-time context, it doesn't matter because the entire call to the consteval function will simply be replaced with the calculated return value.

Advanced note: We cover auto return types in the lesson on type deduction for functions. We cover abbreviated function templates (auto parameters) in the lesson on function templates with multiple template types.

Summary

Forcing compile-time evaluation: No standard way to make constexpr functions prefer compile-time evaluation. Can force compile-time evaluation on per-call basis by using return value where constant expression is required.

Consteval functions (C++20): Immediate functions marked with consteval that must evaluate at compile-time. Compile error results if compile-time evaluation isn't possible.

std::is_constant_evaluated(): Returns true when function executes in constant-evaluated context (where constant expression is required), not necessarily when evaluated at compile-time. Useful for providing different behavior in constant vs. non-constant contexts.

if consteval (C++23): Cleaner replacement for if (std::is_constant_evaluated()) with same semantics.

FORCE_COMPILE_TIME technique: Using consteval wrapper functions or lambdas to force constexpr functions to evaluate at compile-time. Works because consteval function arguments are manifestly constant evaluated.

When to use consteval:

  • Function must evaluate at compile-time (e.g., performs compile-time-only operations)
  • Want to guarantee compile-time evaluation and get compile error if not possible

Key understanding: std::is_constant_evaluated() indicates "compiler forced to evaluate at compile-time" (constant context), not "evaluating at compile-time" (which compilers may do opportunistically).

Understanding consteval and compile-time forcing techniques gives you fine-grained control over when functions evaluate, enabling more sophisticated compile-time programming patterns.