This is the second article in series C++ Metaprogramming series, you can find the first article here C++ Metaprogramming: Variadic Templates & Fold Expressions. This article will unveil the practical usage of templates and constexpr
for compile-time code execution.
Compile-time calculations in C++ allow some operations to be performed during compilation instead of at runtime. This provides a number of advantages: validating data and logic before running the program, reducing overall execution time, and enhancing code safety and reliability. At the same time, of course, there is an additional cost: compilation time could increase significantly, and the code itself may become more difficult to read.
In this article we will explore:
constexpr
)constexpr
(including objects, loops, and algorithms)Historically in C++ metaprogramming emerged thanks to templates. They originally were intended for generating generic functions and classes, but over time it was discovered, that recursive templates could be used to preform quite complex computations.
#include <cstdio>
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
int main() {
printf("%d\n", Factorial<5>::value);
return 0;
}
Here, the computation of Factorial<5>::value
takes place at compile time. However, this approach is difficult to read, requires specializations and overloads the compiler.
#include <cstdio>
template <int N>
struct Fibonacci {
static constexpr int value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};
template <>
struct Fibonacci<0> {
static constexpr int value = 0;
};
template <>
struct Fibonacci<1> {
static constexpr int value = 1;
};
int main() {
printf("%d\n", Fibonacci<10>::value);
return 0;
}
Recursive templates are good for demonstrating ideas, but in real enterprise code they quickly become cumbersome.
Nevertheless, templates metaprogramming is still successfully used today, including in the modern libraries, though more often for working with types than for purely mathematical operations.
constexpr
With the introduction of the constexpr
specifier in C++11, template hacks began to take a back seat. The keyword constexpr
informs the compiler that a function or an object can (and should, if possible) be evaluated at compile time.
constexpr
constexpr
functions: They’re regular functions that can be evaluated at compile time if all arguments are known at the compile stage.constexpr
variables: Variables that are initialized by the constant expression.constexpr
functions, operations that require runtime evaluation - such as a memory allocation with new
or calling virtual functions - cannot be used.constexpr
#include <cstdio>
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
printf("%d\n", factorial(5));
return 0;
}
This core is simpler than the template-based version and easier to read. Moreover, if factorial(5)
is called with a constant, the result will be computed at compile time.
constexpr
Conditional operators (if, switch) inside constexpr
functions have been available since C++14, which simplifies writing complex logic.
Loops (for, while) have also been allowed inside constexpr
since C++14, which makes it easier to create initializer lists, arrays and lookup tables.
constexpr
classes: you can declare class constructors and methods as constexpr
, which allows creating objects at compile time and invoke their methods.
constexpr
and loops#include <cstdio>
constexpr int sum_to_n(int n) {
int sum = 0;
for (int i = 1; i <= n; ++i) {
sum += i;
}
return sum;
}
int main() {
printf("%d\n", sum_to_n(15));
return 0;
}
constexpr
and classes#include <cstdio>
#include <cmath>
struct Point {
int x, y;
constexpr Point(int x, int y) : x(x), y(y) {}
constexpr float len() const { return sqrt(x * x + y * y); }
};
constexpr Point p(3, 4);
int main() {
printf("%f\n", p.len());
return 0;
}
static_assert
: allows performing checks at compile-time
static_assert(sizeof(void*) == 8, "64-bit platform expected");
or using Example 4
static_assert(sum_to_n(15) == 120, "Something went wrong");
if constexpr
(C++17): simplified form of conditional operators in templates
#include <iostream>
template <typename T>
void print_type_info(const T& val) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer: " << val << std::endl;
} else {
std::cout << "Not an integer" << std::endl;
}
}
int main() {
print_type_info(15);
return 0;
}
Here, the brunch that doesn’t satisfy the condition won’t be compiled at all if the check occurs at compile time.
Compile-time array and table generation: convenient for precomputing values
constexpr int squares[] = {
1*1, 2*2, 3*3, 4*4, 5*5
};
Creating complex structures: you can declare an entire object as constexpr
if all of its initialization methods are also constexpr
.
constexpr
Although constexpr
often simplifies the task, there are situations in which templates remain necessary:
std::tuple
or custom structures)Often it is more efficient and clear to perform all calculations using constexpr
, and to use templates only where metaprogramming on types is truly needed. This approach strikes a balance between flexibility and ease of debugging.
constexpr
can significantly increase build times.constexpr
functions requiring full runtime support are prohibited.constexpr
functions, unexpected copies may occur if methods aren’t declared as constexpr
or aren't properly optimized.constexpr
function, it’s not always easy to pinpoint the exact location.Modern C++ offers developers a wide range of tools for organizing compile-time computations - from classic template-based metaprogramming to the more intuitive and flexible constexpr
. When used wisely, these tools can significantly improve code performance and safety by catching entire classes of errors early.
At the same time, it's important to weigh the trade-offs: will your code turn into an unreadable monolith, or will compilation times become unreasonably long? Within reasonable limits, metaprogramming and constexpr
can make your project more efficient and reliable, offering tangible benefits when developing large-scale systems.
If you want to explore these examples hands-on, feel free to visit my GitHub repository where you’ll find all the source files for the code in this article. You can clone the repository, open the code in your favorite IDE or build system, and experiment with constexpr
to see how the compiler works with it. Enjoy playing around with the examples!