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:
- Compile-Time Context: If the interpreter is
actively evaluating an expression to produce a compile-time constant
(e.g., inside a template argument, a
constexprvariable initializer, or astatic_assert), the built-in immediately evaluates totrue. - Runtime Context: If the function is being compiled
into machine instructions for normal runtime execution, the compiler
substitutes
falsefor 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
constexprandconstevalvariables. - Non-type template arguments (NTTP).
- Array bounds in direct type declarations.
- Conditions inside
if constexprstatements. - Arguments to
static_assert. - Expressions inside
noexceptspecifications.
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.