Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Advanced constexpr Function Techniques
Use loops, local variables, and other features inside constexpr functions.
Constexpr Functions Part 2
Constexpr Function Calls in Non-Required Constant Expressions
You might expect a constexpr function would evaluate at compile-time whenever possible, but unfortunately this isn't the case.
In the lesson on constant expressions, we noted that in contexts that don't require a constant expression, the compiler may choose whether to evaluate a constant expression at either compile-time or runtime. Accordingly, any constexpr function call that's part of a non-required constant expression may be evaluated at either compile-time or runtime.
Example:
#include <iostream>
constexpr int processValue(int x)
{
return x;
}
int main()
{
int result { processValue(42) }; // may evaluate at runtime or compile-time
return 0;
}
In the above example, because processValue() is constexpr, the call processValue(42) is a constant expression. However, because variable result isn't constexpr, it doesn't require a constant expression initializer. So even though we've provided a constant expression initializer, the compiler is free to choose whether processValue(42) evaluates at runtime or compile-time.
Core Understanding: Compile-time evaluation of constexpr functions is only guaranteed when a constant expression is required.
Diagnosis of Constexpr Functions in Required Constant Expressions
The compiler is not required to determine whether a constexpr function is evaluatable at compile-time until it's actually evaluated at compile-time. It's fairly easy to write a constexpr function that compiles successfully for runtime use but then fails to compile when evaluated at compile-time.
As a silly example:
#include <iostream>
int processValue(int x)
{
return x;
}
// This function can be evaluated at runtime
// When evaluated at compile-time, the function will produce a compilation error
// because the call to processValue(x) can't be resolved at compile-time
constexpr int calculate(int x)
{
if (x < 0) return 0; // needed prior to adoption of P2448R1 in C++23 (see note below)
return processValue(x); // call to non-constexpr function here
}
int main()
{
int m { calculate(10) }; // OK: evaluates at runtime
constexpr int n { calculate(10) }; // compile error: calculate(10) can't evaluate at compile-time
return 0;
}
In the above example, when calculate(10) is used as an initializer for non-constexpr variable m, it evaluates at runtime. This works fine and returns the value 10.
However, when calculate(10) is used as an initializer for constexpr variable n, it must be evaluated at compile-time. At that point, the compiler determines that the call to calculate(10) can't be evaluated at compile-time, as processValue() isn't a constexpr function.
Therefore, when writing a constexpr function, always explicitly test that it compiles when evaluated at compile-time (by calling it in a context where a constant expression is required, such as in the initialization of a constexpr variable).
Best practice: All constexpr functions should be evaluatable at compile-time, as they will be required to do so in contexts requiring a constant expression.
Always test your constexpr functions in a context requiring a constant expression, as the constexpr function may work when evaluated at runtime but fail when evaluated at compile-time.
For advanced readers: Prior to C++23, if no argument values exist that would allow a constexpr function to be evaluated at compile-time, the program is ill-formed (no diagnostic required). Without the line if (x < 0) return 0, the above example would contain no set of arguments that allow the function to be evaluatable at compile-time, making the program ill-formed. Given that no diagnostic is required, the compiler may not enforce this.
This requirement was revoked in C++23 (P2448R1).
Constexpr/Consteval Function Parameters Are Not Constexpr
The parameters of a constexpr function are not implicitly constexpr, nor may they be declared as constexpr.
Core Understanding: A constexpr function parameter would imply the function could only be called with a constexpr argument. But this isn't the case -- constexpr functions can be called with non-constexpr arguments when the function is evaluated at runtime.
Because such parameters are not constexpr, they cannot be used in constant expressions within the function.
consteval int processData(int value) // value is not constexpr, and can't be used in constant expressions
{
return value;
}
constexpr int transform(int input) // input is not constexpr, and can't be used in constant expressions
{
constexpr int input2 { input }; // compile error: constexpr variable requires constant expression initializer
return processData(input); // compile error: consteval function call requires constant expression argument
}
int main()
{
constexpr int data { 25 };
std::cout << transform(data); // OK: constant expression data can be used as argument to constexpr function transform()
return 0;
}
In the above example, function parameter input isn't constexpr (even though argument data is a constant expression). This means input cannot be used anywhere a constant expression is required, such as the initializer for a constexpr variable (e.g., input2) or in a call to a consteval function (processData(input)).
The parameters of constexpr functions may be declared as const, in which case they're treated as runtime constants.
Related content: If you need parameters that are constant expressions, see the later lesson on non-type template parameters.
Constexpr Functions Are Implicitly Inline
When a constexpr function is evaluated at compile-time, the compiler must be able to see the full definition of the constexpr function prior to such function calls (so it can perform the evaluation itself). A forward declaration won't suffice in this case, even if the actual function definition appears later in the same compilation unit.
This means a constexpr function called in multiple files needs to have its definition included into each translation unit -- which would normally be a violation of the one-definition rule. To avoid such problems, constexpr functions are implicitly inline, making them exempt from the one-definition rule.
As a result, constexpr functions are often defined in header files, so they can be #included into any .cpp file requiring the full definition.
Rule: The compiler must be able to see the full definition of a constexpr (or consteval) function, not just a forward declaration.
Best practice: Constexpr/consteval functions used in a single source file (.cpp) should be defined in the source file above where they're used.
Constexpr/consteval functions used in multiple source files should be defined in a header file so they can be included into each source file.
For constexpr function calls that are only evaluated at runtime, a forward declaration is sufficient to satisfy the compiler. This means you can use a forward declaration to call a constexpr function defined in another translation unit, but only if you invoke it in a context that doesn't require compile-time evaluation.
For advanced readers: Per CWG2166, the actual requirement for the forward declaration of constexpr functions evaluated at compile-time is that "the constexpr function must be defined prior to the outermost evaluation that eventually results in the invocation". Therefore, this is allowed:
#include <iostream>
constexpr int calculate(int);
constexpr int process(int value)
{
return calculate(value); // note that calculate is not defined yet
}
constexpr int calculate(int input) // OK because calculate is still defined before any calls to process
{
return input;
}
int main()
{
constexpr int result{ process(25) }; // this is the outermost invocation
return 0;
}
The intent here is to allow mutually recursive constexpr functions (where two constexpr functions call each other), which wouldn't be possible otherwise.
Recap
Marking a function as constexpr means it can be used in a constant expression. It doesn't mean "will evaluate at compile-time".
A constant expression (which may contain constexpr function calls) is only required to evaluate at compile-time in contexts where a constant expression is required.
In contexts that don't require a constant expression, the compiler may choose whether to evaluate a constant expression (which may contain constexpr function calls) at compile-time or runtime.
A runtime (non-constant) expression (which may contain constexpr function calls or non-constexpr function calls) will evaluate at runtime.
Another Example
Let's examine another example to explore how a constexpr function is required or likely to evaluate further:
#include <iostream>
constexpr int maximum(int p, int q)
{
return (p > q ? p : q);
}
int main()
{
constexpr int max1 { maximum(10, 15) }; // case 1: always evaluated at compile-time
std::cout << max1 << " is larger!\n";
std::cout << maximum(10, 15) << " is larger!\n"; // case 2: may be evaluated at either runtime or compile-time
int a{ 10 }; // not constexpr but value is known at compile-time
std::cout << maximum(a, 15) << " is larger!\n"; // case 3: likely evaluated at runtime
std::cin >> a;
std::cout << maximum(a, 15) << " is larger!\n"; // case 4: always evaluated at runtime
return 0;
}
In case 1, we're calling maximum() in a context requiring a constant expression. Thus maximum() must be evaluated at compile-time.
In case 2, the maximum() function is being called in a context that doesn't require a constant expression, as output statements must execute at runtime. However, since the arguments are constant expressions, the function is eligible to be evaluated at compile-time. Thus the compiler is free to choose whether this call to maximum() will be evaluated at compile-time or runtime.
In case 3, we're calling maximum() with one argument that's not a constant expression. So this will typically execute at runtime.
However, this argument has a value known at compile-time. Under the as-if rule, the compiler could decide to treat the evaluation of a as a constant expression, and evaluate this call to maximum() at compile-time. But more likely, it will evaluate it at runtime.
Related content: We cover the as-if rule in the lesson on constant expressions.
Note that even non-constexpr functions could be evaluated at compile-time under the as-if rule!
In case 4, the value of argument a can't be known at compile-time, so this call to maximum() will always evaluate at runtime.
Put another way, we can categorize the likelihood that a function will actually be evaluated at compile-time as follows:
Always (required by the standard):
- Constexpr function is called where constant expression is required.
- Constexpr function is called from other function being evaluated at compile-time.
Probably (there's little reason not to):
- Constexpr function is called where constant expression isn't required, all arguments are constant expressions.
Possibly (if optimized under the as-if rule):
- Constexpr function is called where constant expression isn't required, some arguments aren't constant expressions but their values are known at compile-time.
- Non-constexpr function capable of being evaluated at compile-time, all arguments are constant expressions.
Never (not possible):
- Constexpr function is called where constant expression isn't required, some arguments have values not known at compile-time.
Note that your compiler's optimization level setting may impact whether it decides to evaluate a function at compile-time or runtime. This also means your compiler may make different choices for debug vs. release builds (as debug builds typically have optimizations turned off).
For example, both gcc and Clang won't compile-time evaluate a constexpr function called where a constant expression isn't required unless the compiler is told to optimize the code (e.g., using the -O2 compiler option).
Advanced note: The compiler might also choose to inline a function call, or even optimize a function call away entirely. Both can affect when (or if) the function call content is evaluated.
Summary
Evaluation guarantees: Constexpr functions are required to evaluate at compile-time only when used in contexts requiring constant expressions (e.g., initializing constexpr variables). In other contexts, the compiler may choose compile-time or runtime evaluation.
Diagnosis timing: Compilers aren't required to verify a constexpr function can evaluate at compile-time until it's actually used in a required constant expression context. Always test constexpr functions in such contexts.
Function parameters: Constexpr/consteval function parameters are not implicitly constexpr and cannot be declared constexpr. They can be used at runtime, so they cannot be constant expressions within the function.
Implicit inline: Constexpr functions are implicitly inline, exempt from the one-definition rule. Define them in header files for multi-file use, or in source files above where they're used for single-file use.
Definition requirement: The compiler must see the full definition of a constexpr function (not just a forward declaration) before compile-time evaluation.
Evaluation likelihood:
- Always: Called where constant expression required, or from other compile-time function
- Probably: Called with constant expression arguments in non-required context
- Possibly: Some arguments not constant expressions but values known at compile-time (as-if rule)
- Never: Some argument values not known at compile-time
Understanding when and how constexpr functions evaluate helps you write effective compile-time code and avoid surprises from unexpected runtime evaluation.
Advanced constexpr Function Techniques - Quiz
Test your understanding of the lesson.
Practice Exercises
Constexpr Function Evaluation Contexts
Create a program demonstrating when constexpr functions evaluate at compile time versus runtime. Test constexpr functions in various contexts and verify their evaluation timing.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!