constexpr Function Best Practices
Apply best practices for writing maintainable constexpr code.
What Can constexpr Functions Actually Do? (Part 4)
Earlier lessons established what constexpr and consteval functions are for. This one is about what you are allowed to write inside them, which is far more than most people expect, and about deciding which of your functions should carry the keyword at all.
The mental model that causes trouble is imagining a compile-time function as something exotic and restricted. It is not. When the compiler evaluates one, it effectively runs the function, which means ordinary imperative code works fine.
Non-const Locals and Modified Parameters Are Fine
A compile-time function can declare local variables that are not constexpr, change them, and modify its own parameters:
#include <iostream>
consteval int transform(int p, int q)
{
p = p + 5;
int result{ p + q };
if (p > q)
result = result - 3;
return result;
}
int main()
{
constexpr int value{ transform(10, 20) };
constexpr int swapped{ transform(30, 20) };
std::cout << value << ' ' << swapped << '\n';
return 0;
}
Output:
35 52
Both results were computed during compilation. The first call makes p 15, sums to 35, and the condition is false. The second makes p 35, sums to 55, and the condition is true, so three comes off.
Parameters Can Feed Other constexpr Calls
A stricter-sounding rule from earlier still applies: when a compile-time function is evaluated at compile time, everything it calls must also be evaluatable at compile time. What surprises people is that a function's own parameters, which are not constexpr, may be passed as arguments to those calls.
The reason is that if the outer call is being evaluated at compile time, the compiler necessarily knows every parameter value, otherwise it could not be doing the evaluation at all. So in this context those values behave as constants:
constexpr int processValue(int value)
{
return value;
}
constexpr int transform(int input)
{
return processValue(input);
}
If transform(25) is evaluated at compile time, the compiler knows input is 25 and can evaluate processValue(input) as processValue(25). If the same call is resolved at run time instead, the inner call is too. The function does not have to be written differently for the two cases.
Calling a Non-constexpr Function
A constexpr function is allowed to call a non-constexpr one, but only in an evaluation that is actually happening at run time. In a constant context the call is an error, because a non-constexpr function cannot produce a compile-time value.
#include <iostream>
int runtimeOnly(int x)
{
return x + 1;
}
constexpr int conditional(bool useRuntime)
{
if (useRuntime)
return runtimeOnly(5);
return 42;
}
int main()
{
int a{ conditional(true) };
constexpr int b{ conditional(true) };
std::cout << a << ' ' << b << '\n';
return 0;
}
The line initializing a is fine, because it runs at run time. The line initializing b demands a compile-time value and therefore fails:
s.cpp: In function 'int main()':
s.cpp:19:33: in 'constexpr' expansion of 'conditional(true)'
19 | constexpr int b{ conditional(true) };
| ~~~~~~~~~~~^~~~~~
s.cpp:11:27: error: call to non-'constexpr' function 'int runtimeOnly(int)'
11 | return runtimeOnly(5);
| ~~~~~~~~~~~^~~
This is exactly why a constexpr function that works when you call it normally can still fail the moment someone uses it in a constant expression.
The legitimate use of this rule is a function that deliberately does something different depending on how it is being evaluated. std::is_constant_evaluated() reports which context you are in:
#include <iostream>
#include <type_traits>
int runtimeVersion()
{
return 1;
}
constexpr int compileTimeVersion()
{
return 2;
}
constexpr int performAction()
{
if (std::is_constant_evaluated())
return compileTimeVersion();
return runtimeVersion();
}
int main()
{
constexpr int atCompileTime{ performAction() };
int atRunTime{ performAction() };
std::cout << "Constant context: " << atCompileTime << '\n';
std::cout << "Runtime context: " << atRunTime << '\n';
return 0;
}
Output:
Constant context: 2
Runtime context: 1
One function, two behaviors, chosen by the compiler. C++23 adds if consteval, which expresses the same idea more directly.
Before C++23, a
constexpr function was technically ill-formed if no set of arguments could produce a constant expression, which is what unconditionally calling a non-constexpr function does. Compilers were never required to diagnose it, so in practice you only found out when you used the function in a constant context. C++23 dropped the requirement.
Three habits follow from all this:
- Avoid calling non-
constexprfunctions from aconstexprfunction where you can. - When the behavior genuinely must differ, branch on
std::is_constant_evaluated(), orif constevalfrom C++23. - Test compile-time functions in a constant context, since passing at run time proves nothing about the other case.
Which Functions Should Be constexpr
The rule of thumb is simple: if a function could be evaluated as part of a required constant expression, mark it constexpr.
A pure function is one where both of the following hold:
- The same arguments always produce the same result
- It has no side effects, so it does not modify static or global state, and does no input or output
Pure functions are the natural candidates, and should generally be constexpr.
Purity is a good guide, not a requirement. C++23 allows a
constexpr function to use and modify static local variables, and since a static local persists between calls, that is a side effect by definition.
Unless there is a specific reason not to, make a function
constexpr when it could be evaluated as part of a constant expression, even if nothing currently calls it that way. A function that cannot be evaluated in a constant expression should not be marked constexpr.
Reasons Not To
constexpr is not free of consequences:
- It is a claim about the function. If the function cannot be used in a constant expression, the keyword is a lie.
- It becomes part of the interface. Once callers rely on it in constant expressions or from their own
constexprfunctions, removing it breaks their code. - It complicates debugging, since you cannot set a breakpoint in or step through an evaluation that happens during compilation.
Why Mark It When It Only Runs at Run Time
A fair question: if every current call happens at run time, what is the point?
The cost is close to zero and the compiler may fold work into the build that would otherwise happen at run time. Beyond that, the argument is about the future. Programs change, and the day someone needs that value in a constant expression, the keyword is either already there or it is a change to an existing function, with the retesting that implies. Writing it once at the start avoids that.
Summary
Non-const locals: compile-time functions may declare and modify non-const locals and may modify their own parameters. The compiler executes the function to get the answer.
Parameters as arguments: a function's parameters may be passed to other constexpr calls, because during a compile-time evaluation the compiler already knows every value.
Non-constexpr calls: permitted only when the evaluation happens at run time. In a constant context, calling a non-constexpr function is a compile error.
Dual behavior: branch on std::is_constant_evaluated(), or if consteval in C++23, when a function must do different things in the two contexts.
Pure functions: same output for the same input, no side effects. These should generally be constexpr.
When not to: when the function cannot appear in a constant expression, when you are not prepared to keep the keyword as part of the interface, or when compile-time evaluation would make debugging harder than the benefit is worth.
Test in a constant context: a compile-time function that works at run time may still fail when a constant expression demands it.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
constexpr Function Best Practices - Quiz
Test your understanding of the lesson.
Practice Exercises
Advanced Constexpr Techniques
Explore advanced constexpr capabilities including using non-const local variables, calling constexpr functions from other constexpr functions, and deciding when to make functions constexpr.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!