How std::is_constant_evaluated Works in C++

Introduced in C++20, std::is_constant_evaluated provides a mechanism for constexpr functions to detect whether they are executing during compile-time constant evaluation or at runtime. This article explains the internal mechanics behind this standard library function, how the compiler tracks constant-evaluation contexts, how the detection avoids common compiler optimization pitfalls, and the evolution of this pattern in modern C++.

The Purpose of Context Detection

A constexpr function can be invoked in two entirely different settings: at compile time (such as calculating the size of a fixed-size array or initializing a constexpr variable) and at runtime (when called with dynamic arguments). Often, the most efficient implementation at runtime relies on low-level primitives like compiler intrinsics, inline assembly, or std::memcpy, none of which are typically permissible in compile-time constant expressions.

std::is_constant_evaluated bridges this gap. It allows a single function implementation to branch between a compile-time-friendly pure C++ algorithm and a hardware-optimized runtime path.

The Underlying Mechanism: Compiler Built-ins

While std::is_constant_evaluated resides in the <type_traits> header, it cannot be implemented using standard template metaprogramming or ordinary C++ language constructs. Instead, it directly wraps a compiler built-in:

  • GCC and Clang use __builtin_is_constant_evaluated()
  • MSVC uses __builtin_is_constant_evaluated()

During the compilation process, the compiler's front end contains an abstract interpreter (often referred to as the constant expression evaluator). As the evaluator traverses the Abstract Syntax Tree (AST), it maintains an internal state machine that tracks whether the current expression is manifestly constant-evaluated.

When the evaluator encounters __builtin_is_constant_evaluated(), it queries this internal state:

  1. Compile-Time Context: If the interpreter is actively evaluating an expression to produce a compile-time constant (e.g., inside a template argument, a constexpr variable initializer, or a static_assert), the built-in immediately evaluates to true.
  2. Runtime Context: If the function is being compiled into machine instructions for normal runtime execution, the compiler substitutes false for the built-in call. Subsequent optimization passes then perform dead-code elimination, stripping out the compile-time branch entirely.

Manifestly Constant-Evaluated Contexts

The C++ standard defines specific scenarios as "manifestly constant-evaluated." Understanding these contexts is crucial to understanding how the compiler decides the return value:

  • Initializers of constexpr and consteval variables.
  • Non-type template arguments (NTTP).
  • Array bounds in direct type declarations.
  • Conditions inside if constexpr statements.
  • Arguments to static_assert.
  • Expressions inside noexcept specifications.

If an expression appears in one of these locations, the compiler forces the constant evaluator to run. When the evaluator encounters std::is_constant_evaluated(), it reliably yields true.

Conversely, if a constexpr function is called inside an ordinary runtime expression without requiring constant initialization, the function will be compiled for runtime, and the built-in yields false.

The Common Pitfall: if constexpr

A frequent mistake is combining std::is_constant_evaluated() with if constexpr:

// INCORRECT
if constexpr (std::is_constant_evaluated()) {
    // This branch ALWAYS executes
}

The condition of an if constexpr statement is itself a manifestly constant-evaluated context. Because the compiler must evaluate the condition at compile time to discard one of the branches, the call to std::is_constant_evaluated() occurs inside a constant-evaluated context. Therefore, if constexpr (std::is_constant_evaluated()) unconditionally evaluates to true, rendering the check useless.

The correct usage requires a standard, non-constexpr if:

// CORRECT
if (std::is_constant_evaluated()) {
    // Compile-time fallback
} else {
    // Runtime-optimized path
}

Because it uses a standard if, both branches must be syntactically valid C++ code, but the compiler's dead-code elimination ensures only the appropriate branch is emitted in the compiled object file.

Evolution in C++23: if consteval

Due to the cognitive overhead and frequent misuse of std::is_constant_evaluated() in if constexpr statements, C++23 introduced dedicated language syntax: if consteval.

Instead of calling a library function wrapping a compiler built-in, developers can write:

if consteval {
    // Manifestly constant-evaluated context
} else {
    // Runtime context
}

if consteval directly hooks into the compiler's constant expression evaluation state without requiring the <type_traits> header, eliminating syntactic ambiguity while maintaining the exact same underlying detection model.