Compile-Time Evaluation

Learn how to implement functionality at compile-time using constexpr and consteval
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
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 (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

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'

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

Immediate Functions (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.

FAQ: Using Compile Time Expressions 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.

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.

Was this lesson useful?

Next Lesson

Variable Templates

An introduction to variable templates, allowing us to create variables at compile time.
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Templates
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Variable Templates

An introduction to variable templates, allowing us to create variables at compile time.
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved