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.
Introduction
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:
- Compilation mechanisms for computations available in C++ (templates and
constexpr
) - Examples of non-trivial use of template metaprogramming
- Modern capabilities of
constexpr
(including objects, loops, and algorithms) - Useful features and techniques, which make developer’s life easier.
- Potential pitfalls and tips on how to avoid them.
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.
Classical examples
Example 1: Factorial using template recursion
#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("%dn", 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.
Example 2: Fibonacci Numbers
#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("%dn", Fibonacci<10>::value);
return 0;
}
Recursive templates are good for demonstrating ideas, but in real enterprise code they quickly become cumbersome.
Drawbacks and limitations
- Debugging complexity: when errors occur in metacode, compiler messages can be extremely confusing.
- Increased compilation time: recursive calculations generate numerous instances of templates.
- Template limitations: difficulties with condition constructs and local variables.
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.
Modern approach: 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.
Basics of 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.- Limitations: in
constexpr
functions, operations that require runtime evaluation – such as a memory allocation withnew
or calling virtual functions – cannot be used.
Example 3: Factorial using constexpr
#include <cstdio>
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
printf("%dn", 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.
Advanced aspects of 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 asconstexpr
, which allows creating objects at compile time and invoke their methods.Example 4:
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("%dn", sum_to_n(15));
return 0;
}
Example 5: 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("%fn", p.len());
return 0;
}
Useful features for compile-time computations
-
static_assert
: allows performing checks at compile-timestatic_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 alsoconstexpr
.
Combining Templates and constexpr
Although constexpr
often simplifies the task, there are situations in which templates remain necessary:
- Need to determine type characteristics, for example determine if a type is an array, a pointer, etc.
- Need to store a compile-time list of types (using
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.
Pitfalls
- Compilation Time: Extensive use of metaprogramming and
constexpr
can significantly increase build times. - Expression Limitations: Not all operations are allowed in
constexpr
functions requiring full runtime support are prohibited. - Potential Hidden Copies: When using complex objects in
constexpr
functions, unexpected copies may occur if methods aren’t declared asconstexpr
or aren’t properly optimized. - Debugging Complexity: If an error occurs inside a
constexpr
function, it’s not always easy to pinpoint the exact location.
Conclusion
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.
Try It Yourself on GitHub
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!
Follow me
Github