AlgoMaster Logo

Template Metaprogramming

Low Priority12 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Template metaprogramming (TMP) is the practice of computing things at compile time using C++ templates, type traits, and constexpr. This lesson is an intro tour, not a deep dive. The goal is enough vocabulary to read modern TMP code, understand legacy TMP code, and know where to look deeper if needed.

What TMP Actually Is

Normal C++ code is turned into instructions that run later. TMP flips that: the code runs inside the compiler during compilation and produces values or types that the final program uses. The output of a TMP computation is baked into the binary. At runtime, there is nothing left to compute.

Three things power modern TMP:

  1. Templates, which let the compiler generate code based on types or values passed in.
  2. `constexpr` functions, which let the compiler evaluate normal-looking functions at compile time.
  3. Type traits (<type_traits>), which allow asking the compiler questions about types and branching on the answers.

The payoff is real but specific. Zero runtime overhead for things that would otherwise need a branch, a virtual call, or a runtime type check. Type-safe generic code that fails to compile with a clear message instead of crashing in production. And in tight numerical code or library work, expensive calculations move out of the runtime entirely.

The diagram shows the split. Normal code goes through the compiler and ends up as runtime instructions. Compile-time computation is resolved by the compiler itself, and the runtime gets the final value without doing the work.

Type Traits: Asking the Compiler About Types

The <type_traits> header is a library of compile-time questions about types. Each trait is a template that, given a type, produces either a value (usually a bool) or a transformed type.

The most common traits:

TraitWhat it tells you
std::is_integral<T>Is T one of the integer types (int, long, char, bool, etc.)?
std::is_floating_point<T>Is T float, double, or long double?
std::is_pointer<T>Is T a pointer type?
std::is_same<T, U>Are T and U exactly the same type?
std::is_class<T>Is T a class or struct?
std::remove_const<T>Returns T with any top-level const stripped off
std::remove_reference<T>Returns T with any reference stripped off
std::decay<T>Strips references, const, and array-to-pointer decay

The original form was clunky. std::is_integral<int>::value returned true. For type-producing traits, typename std::remove_const<T>::type was required. C++17 added _v and _t shortcuts that cut a lot of the noise:

For traits that produce a type, the C++17 shortcut is _t:

Both produce a plain int. The second form is preferred in new code.

A type trait by itself does not do much. The real power shows up combined with if constexpr or static_assert to make decisions based on types.

Type traits have zero runtime cost. The compiler resolves them while compiling. They cost only compile time, and even that is usually negligible.

Compile-Time Branching with if constexpr

Before C++17, branching on a type at compile time was painful. The options were two overloads with overload resolution picking one (tag dispatch), or SFINAE tricks that produced error messages no one wanted to read.

if constexpr cleaned all of that up. It looks like a regular if, but the condition has to be a compile-time constant, and the branch the compiler picks is the only one it compiles for that instantiation. The other branch is not just skipped at runtime, it is discarded entirely.

A serialize function that handles strings differently from numbers:

The mechanism: when the compiler instantiates serialize<int>, it sees that std::is_same_v<int, std::string> is false, so it throws away the string branch entirely. The "\"" + value + "\"" line never has to be valid for an int. With a regular if, that line would have to compile for every instantiation, even the unreachable ones, and it would not.

The diagram shows how if constexpr works during template instantiation. Each path keeps one branch and discards the rest. The discarded code never has to be valid for that type.

if constexpr replaces a large portion of what code used to express with SFINAE. For branching on a compile-time condition inside a template body, use if constexpr first. Save SFINAE and concepts for constraining the template's signature itself, not its body.

The condition inside if constexpr (...) must be constexpr-evaluatable. if constexpr (someRuntimeBool) is not allowed. The compiler needs to know the answer at compile time.

constexpr Functions: Real Functions That Also Run at Compile Time

A constexpr function looks like a normal function but carries a promise: with compile-time arguments, the compiler can evaluate it at compile time. With runtime arguments, it runs at runtime like any other function. Same code, two contexts.

A compile-time discount calculation for a shopping cart:

Assigning the result to a constexpr variable forces the compiler to evaluate at compile time. With runtime values as arguments, the same function runs at runtime. There is no need to maintain two versions.

The classic compile-time example is factorial. With constexpr, it is straightforward:

F5 is the literal 120 in the compiled binary. There is no recursion at runtime.

A constexpr function called with compile-time arguments costs zero runtime cycles. Called with runtime arguments, it costs the same as any normal function. The keyword unlocks the compile-time path without breaking the runtime one.

The Old Way: Recursive Template Metafunctions

Before constexpr, the only way to compute at compile time was recursive templates. This pattern still appears in older libraries, so it is worth understanding once.

A template metafunction is a template (usually a struct) that holds a static constexpr value or a using alias, computed by recursively instantiating itself with different arguments. The classic factorial:

The compiler chases the recursion: Factorial<5> instantiates Factorial<4>, which instantiates Factorial<3>, all the way down to the Factorial<0> specialization, which stops the chain. Each instantiation is a separate type in the compiler's memory.

This works, but it is awkward. It generates a fresh type for every step. The syntax is heavier than a normal function. Error messages from deep instantiation are known for being unreadable. And a separate specialization is needed for every base case.

The same thing as a constexpr function:

That is all. Same compile-time evaluation, no template instantiation chain, error messages that point to real lines.

The takeaway: Recognize the recursive-template style when reading older code, but write the constexpr version. The old style is preserved here for historical literacy, not as something to imitate.

static_assert: Compile-Time Sanity Checks

static_assert checks a condition at compile time. If the condition is false, compilation fails with the given message. It turns "this should never happen" into "this will not even compile".

The condition has to be a constexpr expression. That is where type traits and constexpr functions apply directly.

If a user tries PriceCalculator<Customer>, the compiler stops with: static assertion failed: PriceCalculator requires a numeric type (int, double, etc.). That is much clearer than a 200-line error about operator+= not being defined for Customer.

static_assert also pairs naturally with sizeof checks. Tight code sometimes needs to know that a struct fits in a cache line or that two layouts have not drifted out of sync:

If a later edit adds a field that pushes Product past 64 bytes, the build fails with a message that names the rule that was broken. That is far cheaper than discovering it during a performance regression later.

Since C++17, static_assert can take a condition with no message: static_assert(condition);. The compiler will print the expression itself. The version with a message is usually clearer.

Compile-Time Arrays with std::array and constexpr

std::array is a thin wrapper around a fixed-size C array, and its constructors and accessors are constexpr. That allows building, filling, and reading arrays entirely at compile time.

A lookup table of tax rates for five regions, computed once at compile time and baked into the binary:

TAX_RATES is initialized at compile time. By the time main runs, the array's contents are already sitting in read-only memory. There is no constructor call, no loop, no runtime cost. The same pattern works for precomputed sin/cos tables, command-name lookup tables, and small constant maps.

This is one of the cleanest payoffs of modern TMP. The code looks like a normal initializer, and the compiler does the work.

Where the Topic Goes Deeper

This lesson is a starting point. Production TMP gets deeper. A few signposts:

  • `std::tuple` and `std::variant` manipulation allow writing code that operates on heterogeneous lists of types. Iterating a tuple element-by-element requires fold expressions and index sequences, both of which are TMP techniques.
  • Boost.Hana is a library by Louis Dionne that treats types as first-class values and provides a functional toolkit for transforming them. It can compute, filter, and transform type lists at compile time with clean syntax.
  • Expression templates are a technique used by libraries like Eigen to fuse matrix operations at compile time. A + B + C does not allocate three temporaries; it builds a template expression tree that the compiler unrolls into a single optimized loop.
  • C++20 concepts and `requires` expressions provide a more disciplined way to constrain templates. They are covered in their own lessons; for this section, the relevant takeaway is that concepts often replace what older code did with SFINAE plus type traits.

None of these are required for good C++ code. For header-only libraries, generic numerical code, or serialization frameworks, TMP shows up fast. The intro here is enough to read the code and decide whether to learn more.

Quiz

Template Metaprogramming Quiz

10 quizzes