Compile-Time Evaluation

Learn how to implement functionality at compile-time using constexpr and consteval

Ryan McCombe
Updated

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:

  • Using constexpr to define compile-time constants and invoke functions at compile-time.
  • The role of consteval in defining immediate functions that can only be called during compilation.
  • Applying constexpr and consteval to constructors, member functions, and operator overloads.

Compile Time Constants Using 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;
}

Constants and const-Correctness

Learn the intricacies of using const and how to apply it in different contexts

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

Invoking Functions at Compile Time

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'

Environment Testing Using 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 false.

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;
}
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);
  }
}

Immediate Functions Using 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 as 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 would require them be shipped to users and invoked at run time.

Compile Time Constructors

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'

Compile Time Member Functions

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()};
}

Compile Time Operators

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};
}

Summary

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.
  • Both constructors and member functions can be marked as constexpr or consteval, enabling compile-time object construction and member function invocation.
  • Operators can also be overloaded with constexpr or consteval specifiers, allowing compile-time evaluation of operator expressions.
  • Compile-time evaluation can lead to optimized code performance, as computations are performed at compile-time rather than at runtime.
Next Lesson
Lesson 24 of 128

Variable Templates

An introduction to variable templates, allowing us to create variables at compile time.

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Best Practices: constexpr vs consteval
Are there any best practices for deciding between constexpr and consteval?
constexpr and consteval with Lambdas
Can I use constexpr or consteval with lambda functions?
constexpr and Dynamic Memory
What happens if I try to use dynamic memory allocation in a constexpr function?
constexpr with Recursive Functions
Can I use constexpr or consteval with recursive functions?
constexpr and Virtual Functions
Can I use constexpr with virtual functions?
Exceptions in constexpr and consteval
How do I handle exceptions in constexpr and consteval functions?
constexpr and consteval with Variadic Functions
Can I use constexpr or consteval with variadic functions or function templates?
constexpr, consteval, and Coroutines
Can I use constexpr or consteval with coroutines?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant