0. Introduction & Environment Setup
Motivation: why care about metaprogramming?
Metaprogramming allows to reuse and execute code at compile time by the use of templates and libraries such as boost. The goal is to make the code faster and cleaner.
Also keep in mind its use in industry, with serialization libraries like protobuf, optimized numerical libs like Eigen.
Development environment: C++ compiler (e.g., g++/clang++), standard version (C++17/20), optional libraries (Boost.MPL, Boost.Hana)
We suppose using a nixOs distro to manage cache libraries:
|
|
and we compile with:
|
|
Context and goals of the article
Epita gives us a first idea of what c++ is all about, and we can see a bit more with the c++ elective but a lot is to experiment with it. The goal of this article is to make you consider using c++ more often with thoses programming strategies, and show all you need to know to masteries Template logic.
1. Getting Started with C++ Templates
Let’s start with some old good syntax
template <parameter-list > requires-clause (optional) declaration
we got the ’template’ keyword, followed by a non-empty comma-separated list, the params can be a constant paramater, a type-parameter or a template parameter.
The requires-clause is optional and enable us to put constraints on the template arguments and follow by a declaration.
We clearly understand templates are useful for non-constant types in objects as classes.
But the point here is to try to abuse the fact that a template is computed at compile time, not runtime.
Introduction to templates: function and class templates
For a introduction, let’s start with a demo:
|
|
The main use of templates is variadic parameters, in this code we see a class edge with a variadic weight, it can be a int, string or whatever.
So let’s go with a basic example. Here, instead of writing a function add_int, add_double, and so on, we can do:
|
|
And now we can use it with int, float, even std::string:
|
|
Here is a quick summary:
|
|
Note putting multiple types at once is also possible:
|
|
All these methods push us to think more deeply about the problem and get creative when solving any kind of issue in C++.
How template instantiation works at compile-time
Let’s dive down a bit about how the instantiation works. First quick reminder: a template is just a blueprint, the compiler doesn’t generate code for it until it’s used.
When you write something like:
|
|
The compiler reads your template edge<T> and creates a version of the class specifically for int. This is the instantiation. If you later write:
|
|
Now it generates a second version of the class for std::string.
You can think of it as macro expansion, but type-aware and checked by the compiler. Also, if you write some code inside the template that only works for integers, but then try to instantiate with std::string, the compiler will fail only when you instantiate, not before.
|
|
This will only produce an error if T is used with a type that doesn’t have <.
So template instantiation is lazy, and contextual:
- It’s only compiled when you use it.
- It’s compiled differently for each type.
- If unused, the code isn’t checked (good and bad at once!).
Simple examples: type traits, compile-time constant functions
Now that we get how templates work, let’s check some small examples we can actually use.
Type traits are like tools to check or modify types at compile time. It’s all in <type_traits>, and really useful.
Quick example:
|
|
This gets evaluated at compile time, so the wrong branch is just removed by the compiler. Super clean and fast.
We also have compile-time functions with constexpr. These are just normal functions but evaluated at compile time (if possible).
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int arr[factorial(5)]; // creates an array of size 120
So, here we compute the factorial at compile time and use the result directly. Saves runtime cost and shows the power of templates mixed with constexpr.
2. From Templates to Metaprogramming
What is metaprogramming in C++?
First i want to quote David A. who wrote :
[…] a metaprogram is a program that manipulates code.[…].Your C++ compiler is one example: it manipulates your C++ code to produce assembly langague or machine code.
Note that in C++, metaprograming was discover almost by accident.
At first, C++ templates were just meant for making generic containers like std::vector.
But developers, being developers, started abusing the system—using recursion and type tricks—until the compiler accidentally became a Turing machine.
And that’s how metaprogramming was born… more by hack than by design!
We can say that the metaprograming in C++ is the abuse of template and more to manipulate code in compile-time.
We do this for:
- speed
- abstraction
- because we can ;p
Static polymorphism and the role of templates
In C++ you have dynamic polymorphism with virtuals:
|
|
But templates allow us to do something like:
|
|
Now speak() is just a function that works on any object that has a .speak() method. This is sometimes called duck typing at compile time.
One point is, no vtable is generated for the method .speak(), so no virtual keyword is needed, the function is considered inline and no runtime dispatch happens.
But there’s a catch:
With static polymorphism, the type must be known at compile time, so you lose runtime flexibility. Also, code size can grow if you instantiate the function for many different types.
Tools: constexpr, if constexpr, type traits like std::enable_if, std::conditional
- constexpr: Marks functions or variables as evaluable at compile time.
|
|
- if constexpr: Compile-time branching.
|
|
- std::enable_if: Enable/disable functions based on compile-time conditions.
|
|
- std::conditional: Compile-time type selection.
|
|
Examples: compile-time factorial, filtering types
Let’s take a simple example: computing a factorial at compile time.
|
|
Now with if constexpr (C++17), we can branch at compile time:
|
|
No instantiation of the unused branch,the compiler skips it completely.
Let’s say we want to enable a function only for floating points:
|
|
Or in C++20 with concepts (cleaner):
|
|
You can do a lot with these techniques, build lists of types, switch implementations, pick algorithms… All before your program even runs.
3. Crossing the Compile-Time / Runtime Boundary
Why mix compile-time and runtime logic?
We can ensure that not everything is known at compile time, we need flexibility. However we still want to push as much logic as possible at compiletime to optimize runtime. In thoses cases, combine both of these two worlds to have flexibility and speed at the same time.
C++ metaprogramming allows us to encode logic that is normally runtime-checked… At compile time. That gives us extra safety and performance. Here we use types to encode units and dimensions (like mass, length, time) directly into the type system.
That means: you can’t add a mass to a length, the compiler will stop you.
Examples of compile-time decision-making affecting runtime
To demonstrate this point here a exemple of the dimensional analysis applied in c++ using templates and boost.
Let’s take Boost.MPL’s vector_c to describe the physical units. It’s a compile-time container of integers.
|
|
Here, each vector_c is a type that represents powers of each base unit. This is 100% compile-time info, there’s no data here.
Now we use it with a runtime value, by attaching it to a quantity<T, Dimensions> wrapper:
|
|
You can do:
|
|
But you can’t assign one to the other:
|
|
That’s what we call static type checking of domain rules, and it’s done before the program even runs.
Using SFINAE or concepts to enable/disable code paths
SFINAE (Substitution Failure Is Not An Error) allows us to enable or disable functions or templates based on type information, at compile time.
You could extend this to define arithmetic only between same-dimension types:
|
|
No std::enable_if or concepts needed here, the type system itself does the check.
But you can imagine more complex behaviors (e.g. multiplying dimensions), where you’d use SFINAE to restrict combinations, or Boost.MPL operations to compute the new dimensions at compile time.
You could also write:
|
|
That’s what SFINAE is: if substitution of template parameters fails, it’s not an error, that overload is just skipped.
Basic use of Boost.MPL or Boost.Hana to manipulate types
As in the example seen before Boost.MPL is considered more old-school but still powerful.
We can build type lists, do logic at compile-time.
On the other hand, Boost.Hana operates on both values and types, and is considered a more modern approach to metaprogramming, designed with C++14/17 features in mind.
Thoses tools are mainly used to:
- filter types
- transform types
- dispatch based on type lists.
|
|
Here:
mpl::int_<N> wraps an integer into a type with a static value field.
mpl::if_c<cond, A, B> selects one of two types based on a compile-time condition.
max_type computes the greater of two mpl::int_values, all at compile time.
result::value is accessible at runtime, but was resolved long before.
This pattern is used heavily in template metaprogramming to manipulate types like numbers, type lists, and traits.
4. Performance and Optimization
Advantages: removing runtime branching, improving specialization
With this kind of methods, The code is lighter at runtime, there is no need for if, switch or else.
The logic is specialized per type with no runtime code added.
Drawbacks: code bloat, compile-time overhead, complexity
Of course there is a catch, generated code and templates make larger binaries, and the compile time increases a lot. Also note that you are never really alone on projects and metaprograming can be harder to read thus debug especially for newbies.
Example: comparing compile-time and runtime dispatch
Let’s compare two cases:
Runtime polymorphism with virtual functions:
|
|
Compile-time dispatch using templates and constexpr:
|
|
Performance differences:
- Virtual functions: Extra runtime cost due to vtable lookup.
- Templates with constexpr: Fully inlined, no runtime overhead.
Conclusion:
If your use case allows compile-time dispatch, prefer it for performance-critical paths. But for flexibility and when dealing with unknown types at runtime, virtual functions still have their place.
Let’s compare two ways to select behavior based on type:
- Runtime dispatch via virtual functions (classic OOP).
- Compile-time dispatch via templates and constexpr (metaprogramming).
Virtual Call (Runtime Dispatch)
|
|
Dispatch happens at runtime. Introduces indirection thus implying slower execution (vtable lookup). Allows polymorphism with different types at runtime.
Template Dispatch (Compile-Time)
|
|
Resolved at compile time means the compiler selects the code path before execution, removing any runtime decision. No vtable implies no indirection, the call gets fully inlined and optimized. Downside: the type must be fully known during compilation, no late binding possible.
Benchmark Overview
Using a simple benchmark,
| Dispatch Type | Time (ms) | Notes |
|---|---|---|
| Virtual Functions | 130 | Cache misses, vtable overhead |
| Templates | 80 | Inlined, branchless |
5. Practical Applications
Real-world use cases in libraries (e.g., Boost, Eigen)
Boost:
- Uses templates everywhere:
type_traits,mpl::if_,hana, etc… - Heavy use of SFINAE and compile-time computation.
- Example:
boost::variantdispatch logic is pure metaprogramming.
Eigen:
- Expression templates to fuse matrix operations -> no temporary objects.
- Operations like
C = A + B * Dare optimized at compile-time.
STL (Standard Template Library):
std::enable_if,std::is_same,iterator_traitsare the template metaprogramming core tools.- Used internally to make containers and algorithms generic and safe.
When to use or avoid metaprogramming in production code
When to use metaprogramming
- When performance is critical (e.g., embedded systems, game engines, math libraries)
- When logic needs to be generic and type-safe
- When you want strong compile-time guarantees
- When optimizing runtime efficiency (e.g., no virtual dispatch, inlining)
When to avoid metaprogramming
- When it makes simple code unreadable (template hell)
- When the team lacks experience with advanced C++ templates
- When compile-time complexity outweighs runtime benefits
- When rapid development and readability are more important than raw performance
Rule of thumb: If it’s not faster or safer, don’t make it a template.
Tips for keeping template code maintainable
Prefer constexpr, consteval, and concepts (C++20) over old-school SFINAE.
Split logic into small, composable metafunctions.
Use static_assert,with meaningful messages ;
static_assert let’s you debug templates and print clear compile-time error messages when constraints aren’t met.
It’s a classic assert but design for template.
Always document what your template expects (T must be a type with .size() etc).
Avoid deeply nested typename hell.
6. Conclusion
Recap of key takeaways
Metaprogramming moves logic to compile time, improving performance and safety. Templates enable abstraction, generic programming, and static checks. Use modern tools: constexpr, concepts, type_traits, Boost.
When metaprogramming is most useful
When is metaprogramming most useful?
- In performance-critical code paths, such as rendering engines or digital signal processing, where every cycle counts.
- Within libraries designed for heavy reuse, including math utilities, container implementations, and serialization frameworks.
- In scenarios where eliminating runtime overhead is essential to meet strict performance or resource constraints.
Final thoughts on clarity, performance, and abstraction
Metaprogramming is like magic: powerful but dangerous. Clarity > cleverness. Abstractions must help both the compiler and the human reader. Good metaprogramming should feel invisible — just fast, safe code.
Bibliography
-
C++ Template Metaprogramming David Abrahams & Aleksey Gurtovoy PDF
-
The C++ Programming Language (4th Edition) Bjarne Stroustrup PDF
-
LSST DM Developer Guide – C++ API Design Rubin Observatory Data Management Online Guide