constexpr
and consteval
In this lesson, we’ll learn how to use the constexpr
and consteval
specifiers to ensure our expressions can be evaluated at compile time.
Compile-time evaluation can significantly improve performance by removing expensive calculations from runtime to compile-time
It additionally unlocks advanced metaprogramming techniques, such as generating template parameters at compile time. We'll cover:
constexpr
to define compile-time constants and invoke functions at compile-time.consteval
in defining immediate functions that can only be called during compilation.constexpr
and consteval
to constructors, member functions, and operator overloads.constexpr
)We covered in our previous lessons how templates are instantiated at compile time. Because of this, any arguments we provide to templates must be known at compile time.
Below, we try to instantiate a template by providing an int
variable as an argument, which results in an error:
template <int SomeInt>
struct Resource {
int Value{SomeInt};
};
int main() {
int SomeValue{3};
Resource<SomeValue> A;
}
error C2971: 'Resource': template parameter 'SomeInt': 'SomeValue': a variable with non-static storage duration cannot be used as a non-type argument
As we introduced in our beginner lessons, we can declare a variable as constexpr
, letting the compiler ensure that the value of the variable can be determined at compile time:
template <int SomeInt>
struct Resource {
int Value{SomeInt};
};
int main() {
constexpr int SomeValue{3};
Resource<SomeValue> A;
}
When we do this, the compiler will prevent us from using any expression in the initialization that cannot be evaluated at compile time.
For example, if we attempt to initialize a constexpr
variable using a function call, we’ll typically get a compilation error:
#include <iostream>
int GetInt() {
return 42;
}
int main() {
constexpr int SomeValue{GetInt()};
}
error C2131: expression did not evaluate to a constant
Similar to variables, we can mark functions as constexpr
. This asks the compiler to ensure that the function can be evaluated at compile time and, as a result, makes the function usable in constexpr
 contexts:
constexpr int GetInt() {
return 42;
}
int main() {
// This now works
constexpr int SomeValue{GetInt()};
}
Value: 3
Because of this, the value returned by a constexpr
function can be used to provide an argument to a template:
#include <iostream>
constexpr int GetInt() {
return 42;
}
template <int SomeInt>
struct Resource {
int Value{SomeInt};
};
int main() {
Resource<GetInt()> A;
std::cout << "Value: " << A.Value;
}
Value: 42
When we have a constexpr
function, the compiler will prevent that function from doing anything that may not be possible at compile time.
For example, our function won’t be able to call other functions (or operators), unless those functions are also marked as constexpr
.
The following won’t work, as the <<
operator on std::cout
is not constexpr
, and therefore cannot be used at compile time:
#include <iostream>
constexpr int GetInt() {
// We can't do this at compile time:
std::cout << "Getting an int";
return 42;
}
int main() {
constexpr int x{GetInt()};
}
error C3615: constexpr function 'GetInt' cannot result in a constant expression
failure was caused by call of undefined function or one not declared 'constexpr'
std::is_constant_evaluated()
When we mark a function as constexpr
, the compiler ensures that our function can be used at compile time. But, that does not mean our function can only be used at compile time.
It is possible for a constexpr
function to be executed at run time. Add()
in the following example demonstrates this:
#include <iostream>
int GetNumber() {
int x;
std::cout << "Enter A Number: ";
std::cin >> x;
return x;
}
constexpr int Add(int x, int y) {
return x + y;
}
int main() {
std::cout << Add(GetNumber(), GetNumber());
}
Enter A Number: 2
Enter A Number: 3
5
When we need to know whether a function is being executed at compile time or run time, we can use the std::is_constant_evaluated()
from the <type_traits>
 header.
It returns true
if it was invoked at compile time, or in other contexts where a constant expression is required, such as array bounds or template arguments. In any other context, std::is_constant_evaluated()
will return true
.
We can use this to change the behaviour of a function based on the context in which it was executed:
#include <iostream>
#include <type_traits>
constexpr int GetValue() {
if (std::is_constant_evaluated()) {
return 1;
} else {
return 2;
}
}
int main() {
constexpr int CompileTimeResult{GetValue()};
std::cout << "Compile time result: "
<< CompileTimeResult;
int RunTimeResult{GetValue()};
std::cout << "\nRun time result: "
<< RunTimeResult;
}
//<o>
Compile time result: 1
Run time result: 2
One of the main use cases for this is performance optimization. For example, if we have a function that performs an expensive calculation, we may prefer that the function prioritise speed at run time, but accuracy at compile time.
So, if std::is_constant_evaluated()
returns true
, we perform the slower but more accurate algorithm, but if it’s false
, we use the algorithm that quickly returns an approximation.
#include <type_traits>
constexpr float Calculate(float x, float y) {
if (std::is_constant_evaluated()) {
return ExactAlgorithm(x, y);
} else {
return Approximation(x, y);
}
}
consteval
)If we want a function to only be usable at compile time, we can mark it as consteval
. This imposes all the same restrictions as constexpr
, but additionally, if an expression would result in a consteval
function being invoked at run time, we will get a compilation error instead:
#include <iostream>
int GetNumber() {
int x;
std::cout << "Enter A Number: ";
std::cin >> x;
return x;
}
consteval int Add(int x, int y) {
return x + y;
}
int main() {
std::cout << Add(GetNumber(), GetNumber());
}
error C7595: 'Add': call to immediate function is not a constant expression
Functions annotated consteval
are sometimes referred to as immediate functions. This is because, once the compiler sees code attempting to invoke a consteval
function, the compiler must perform the invocation immediately. It cannot be invoked later, at run time.
When working on larger projects, our build process can get quite elaborate. For example, our program may need to perform expensive calculations or format large amounts of supporting data.
By marking the functions that perform tasks like this consteval
, we make their purpose clearer. Additionally, the compiler ensures nobody uses these functions in a context that requires them be shipped to users and invoked at run time.
Even when an expression is evaluated at compile time, the result of that evaluation can still be available at run time. Below, we have the result of a consteval
function call being used to display output at run time:
#include <iostream>
consteval int Add(int x, int y) {
return x + y;
}
int main() {
std::cout << Add(1, 2);
}
3
To understand how this works, we can imagine the compiler replacing every consteval
expression in our code with the result of evaluating that expression.
In the previous example, Add(1, 2)
would get replaced with 3
at compile time. This means that at run time, it’s as if the body of our main
function was simply std::cout << 3;
and the Add()
function never existed.
Many object types can be constructed at compile time. We’ve seen examples of this using simple literals, and also standard library containers:
#include <utility>
int main() {
constexpr int SomeInt{42};
constexpr std::pair SomePair{42, true};
}
When working with our custom types, we can mark constructors as constexpr
or consteval
. As such, that constructor can be invoked at compile time, meaning instances of our user-defined type can be created at compile time:
struct SomeType {
constexpr SomeType(int init) : Value{init} {}
int Value;
};
int main() {
constexpr SomeType SomeObject{42};
}
Like any other function, the compiler will prevent us from doing anything within a constexpr
or consteval
constructor that might not be possible at compile time, such as calling a non-constexpr function or operator:
#include <iostream>
struct SomeType {
constexpr SomeType(int init) : Value{init} {
std::cout << "Hello World";
}
int Value;
};
int main() {
constexpr SomeType SomeObject{42};
}
error C3615: constexpr function 'SomeType::SomeType' cannot result in a constant expression
failure was caused by call of undefined function or one not declared 'constexpr'
Member functions can be made constexpr
or consteval
, with the same syntax and effect as any other function:
class SomeType {
public:
constexpr SomeType(int init) : Value{init} {}
constexpr int GetValue() const {
return Value;
}
private:
int Value;
};
int main() {
constexpr SomeType SomeObject{42};
constexpr int SomeInt{SomeObject.GetValue()};
}
Finally, operators can also be marked as constexpr
or consteval
:
struct SomeType {
constexpr SomeType(int init) : Value{init} {}
constexpr int operator+(int Other) const {
return Value + Other;
}
int Value;
};
int main() {
constexpr SomeType SomeObject{42};
constexpr int SomeInt{SomeObject + 2};
}
In this lesson, we learned that the constexpr
and consteval
keywords are used to enable compile-time evaluation of expressions, functions, and object constructions. Key points:
constexpr
variables and functions can be evaluated at compile-time if their initializers and expressions can be resolved as constant expressions.constexpr
functions can be invoked at both compile-time and runtime.consteval
functions, also known as immediate functions, can only be invoked at compile-time, preventing accidental runtime invocation.constexpr
or consteval
, enabling compile-time object construction and member function invocation.constexpr
or consteval
specifiers, allowing compile-time evaluation of operator expressions.Learn how to implement functionality at compile-time using constexpr
and consteval
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.