Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Compile-Time Function Evaluation
Write functions that can be evaluated at compile time when given constant arguments.
Constexpr Functions
In the lesson on constexpr variables, we introduced the constexpr keyword for creating compile-time (symbolic) constants. We also introduced constant expressions, which are expressions that can be evaluated at compile-time rather than runtime.
One challenge with constant expressions is that function calls to normal functions are not allowed in constant expressions. This means we cannot use such function calls anywhere a constant expression is required.
Consider this program:
#include <iostream>
int main()
{
constexpr double sideLength { 5.0 };
constexpr double pi { 3.14159265359 };
constexpr double perimeter { 4.0 * sideLength };
std::cout << "Our square has perimeter " << perimeter << "\n";
return 0;
}
This produces the result:
Our square has perimeter 20
Having a complex initializer for perimeter isn't great (and requires us to instantiate a supporting variable, sideLength). So let's make it a function instead:
#include <iostream>
double calculatePerimeter(double side)
{
return 4.0 * side;
}
int main()
{
constexpr double perimeter { calculatePerimeter(5.0) }; // compile error
std::cout << "Our square has perimeter " << perimeter << "\n";
return 0;
}
This code is much cleaner. It also doesn't compile. Constexpr variable perimeter requires its initializer to be a constant expression, and the call calculatePerimeter() isn't a constant expression.
In this particular case, we could make perimeter non-constexpr, and the program would compile. While we'd lose the benefits of constant expressions, at least the program would run.
However, there are other cases in C++ (which we'll introduce in the future) where we don't have alternate options available, and only a constant expression will do. In those cases, we'd really like to be able to use functions, but calls to normal functions just won't work. So what can we do?
Constexpr Functions Can Be Used in Constant Expressions
A constexpr function is a function allowed to be called in a constant expression.
To make a function a constexpr function, we simply use the constexpr keyword in front of the function's return type.
Core Understanding: The constexpr keyword signals to the compiler and other developers that a function can be used in a constant expression.
Here's the same example as above, but using a constexpr function:
#include <iostream>
constexpr double calculatePerimeter(double side) // now a constexpr function
{
constexpr double sidesInSquare { 4.0 };
return sidesInSquare * side;
}
int main()
{
constexpr double perimeter { calculatePerimeter(5.0) }; // now compiles
std::cout << "Our square has perimeter " << perimeter << "\n";
return 0;
}
Because calculatePerimeter() is now a constexpr function, it can be used in a constant expression, such as the initializer of perimeter.
Constexpr Functions Can Be Evaluated at Compile Time
In the lesson on constant expressions, we noted that in contexts requiring a constant expression (such as the initialization of a constexpr variable), a constant expression is required to evaluate at compile-time. If a required constant expression contains a constexpr function call, that constexpr function call must evaluate at compile-time.
In our example above, variable perimeter is constexpr and thus requires a constant expression initializer. Since calculatePerimeter() is part of this required constant expression, calculatePerimeter() must be evaluated at compile-time.
When a function call is evaluated at compile-time, the compiler will calculate the function call's return value at compile-time, and then replace the function call with the return value.
So in our example, the call to calculatePerimeter(5.0) is replaced with the function call result, which is 20.0. In other words, the compiler will compile this:
#include <iostream>
constexpr double calculatePerimeter(double side)
{
constexpr double sidesInSquare { 4.0 };
return sidesInSquare * side;
}
int main()
{
constexpr double perimeter { 20.0 };
std::cout << "Our square has perimeter " << perimeter << "\n";
return 0;
}
To evaluate at compile-time, two other things must also be true:
- The call to the constexpr function must have arguments known at compile time (e.g., are constant expressions).
- All statements and expressions within the constexpr function must be evaluatable at compile-time.
When a constexpr (or consteval) function is being evaluated at compile-time, any other functions it calls are required to be evaluated at compile-time (otherwise the initial function wouldn't be able to return a result at compile-time).
For advanced readers: There are some other lesser encountered criteria as well. These can be found on cppreference.com.
Constexpr Functions Can Also Be Evaluated at Runtime
Constexpr functions can also be evaluated at runtime, in which case they will return a non-constexpr result. Example:
#include <iostream>
constexpr int maximum(int p, int q)
{
return (p > q ? p : q);
}
int main()
{
int p{ 10 }; // not constexpr
int q{ 15 }; // not constexpr
std::cout << maximum(p, q) << " is larger!\n"; // will be evaluated at runtime
return 0;
}
In this example, because arguments p and q are not constant expressions, the function cannot be resolved at compile-time. However, the function will still be resolved at runtime, returning the expected value as a non-constexpr int.
Core Understanding: When a constexpr function evaluates at runtime, it evaluates just like a normal (non-constexpr) function would. In other words, the constexpr has no effect in this case.
Allowing functions with a constexpr return type to be evaluated at either compile-time or runtime enables a single function to serve both cases.
Otherwise, you'd need separate functions (a function with a constexpr return type, and a function with a non-constexpr return type). This would not only require duplicate code, the two functions would also need different names!
Remind Me Again Why We Care Whether Our Functions Execute at Compile-Time?
Now would be a great time to review the benefits that compile-time programming techniques can provide from the earlier lesson on constant expressions.
Summary
Constexpr functions: Functions marked with constexpr that can be evaluated at compile-time when used in contexts requiring constant expressions.
Dual-mode evaluation: Constexpr functions can evaluate at either compile-time (when arguments are constant expressions and used in required constant expression contexts) or runtime (when arguments aren't constant expressions or not used in required contexts).
Requirements for compile-time evaluation:
- Function must be marked
constexpr - All arguments must be constant expressions
- All statements and expressions within the function must be evaluatable at compile-time
- Called in a context requiring a constant expression (e.g., initializing a constexpr variable)
Benefits:
- Enables using function calls in constant expressions
- Single function serves both compile-time and runtime needs
- Improves performance by moving calculations to compile-time when possible
- Reduces code duplication (no need for separate compile-time and runtime versions)
Key understanding: The constexpr keyword signals that a function can be used in a constant expression, not that it will evaluate at compile-time. Actual compile-time evaluation depends on context and arguments.
Understanding constexpr functions opens the door to compile-time programming in C++, allowing you to write more efficient code and use functions in contexts that previously required hardcoded values or macros.
Compile-Time Function Evaluation - Quiz
Test your understanding of the lesson.
Practice Exercises
Constexpr Functions
Create functions that can be evaluated at compile time. Learn the rules and benefits of constexpr functions.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!