11 Template metaprogramming
This chapter shows how C++ templates enable programs that run at compile time to manipulate types and guide code generation, yielding faster and safer generic libraries. It motivates metaprogramming as a way to tailor algorithms to type capabilities and highlights modern C++17 features that make this style practical: compile-time branching with constexpr if, rich type traits, and SFINAE-based introspection. Alongside type-centric techniques, the chapter also emphasizes uniformly invoking arbitrary callables and illustrates how these tools support higher-level abstractions, from generic utilities to small domain-specific languages.
It begins with deducing and transforming types: using decltype and std::declval to infer element types, then normalizing results with remove_cv and remove_reference. The chapter demonstrates verifying deductions via static_assert and building core traits by partial specialization (for example, custom is_same and remove_reference). It then advocates relying on container-provided value_type when available, and shows how to detect such metainformation and other properties (like iterability) with void_t and SFINAE. Combined with constexpr if, these checks let code select the right implementation while compiling only valid branches, preventing hard-to-read overload sets and eliminating invalid code paths.
Next, the text develops a generic currying wrapper that stores a callable and captured arguments, decides at compile time (with is_invocable) whether enough arguments are present, and either invokes or returns a new curried object. To support every kind of callable—including member pointers—it uses std::invoke, and pairs it with std::apply to expand argument tuples; reference wrappers avoid unnecessary copies. Finally, it builds a compact DSL for transactional record updates by modeling an AST (field, update, transaction), overloading operators to compose updates, applying them atomically via copy-and-swap, and distinguishing setters that return bool from those that return void using is_invocable_r. Together, these patterns showcase how template metaprogramming and C++17 features enable expressive, type-safe APIs with zero runtime overhead.
When you provide different arguments to is_same, only the first definition that returns false_type will be a match. If you pass the same type as both parameters, both definitions will be a match, and the more specialized match will win—the one returning true_type.
The void_t metafunction is strange: it ignores all its parameters and always returns void. It’s useful because it can be evaluated only if the types you pass as its arguments are valid. If they aren’t, void_t triggers a substitution failure, and the compiler will ignore the definition that used it.
The abstract syntax tree you want to model contains three levels: the object you’re updating, a list of updates that need to be performed (if each update contains two items, the field that should be updated), and the new value the field should have.
Summary
- Templates give you a Turing-complete programming language that is executed during program compilation. This was discovered accidentally by Erwin Unruh, who created a C++ program that prints the first 10 prime numbers during compilation—as compilation errors.
- TMP is not only a Turing-complete language; it’s also a pure functional language. All variables are immutable, and there’s no mutable state in any form.
- The
type_traitsheader contains many useful metafunctions for type manipulation. - From time to time, because of limitations or missing features in the core programming language, workarounds are added to the standard library. For example,
std::invokeallows you to call all function-like objects, even those that don’t support the regular function-call syntax. - DSLs are tedious to write but let you significantly simplify the main program logic. Ranges are also DSLs, in a sense; they define an AST for defining range transformations using the pipe syntax.
FAQ
What is template metaprogramming (TMP) in C++, and why would I use it?
Template metaprogramming is writing code that runs during compilation to compute with types and generate specialized code. It’s useful for optimized generic code: you can select algorithms, check type properties, and transform types at compile-time, leading to better performance and safer APIs without runtime overhead.What is a metafunction, and how is it different from a normal function?
A metafunction is a template that operates on types (and sometimes integral constants) instead of runtime values. It “returns” a type (e.g., via a nested::type or a using alias) or a compile-time value (e.g., std::true_type/std::false_type). You invoke it by instantiating the template, not by calling it at runtime.How can I get the element type of a collection at compile time?
Use a metafunction based ondecltype and iterators, and strip qualifiers: using contained_type_t<C> = std::remove_cv_t<std::remove_reference_t<decltype(*begin(std::declval<C>()))>>; It works even when the collection isn’t default-constructible because it uses std::declval. Prefer a collection’s C::value_type when available.Why does decltype(*begin(c)) give me const T& instead of T?
decltype preserves cv-qualifiers and references; dereferencing an iterator typically yields a reference (often const T&). If you want the “plain” type, remove qualifiers and references with std::remove_cv_t and std::remove_reference_t.How do I “debug” a deduced type at compile time?
- Instantiate an incomplete template to force the compiler to print the deduced type:template<typename T> class error; error<SomeType> x;- Assert expected types with
static_assert(std::is_same<A,B>::value, "message"); (or the C++17 shortcut std::is_same_v<A,B>).What is void_t and how does it relate to SFINAE?
template<typename...> using void_t = void; It always aliases void but only if its arguments are well-formed. In SFINAE contexts this lets you detect traits. Example: detect nested value_type by specializing on void_t<typename C::value_type> to choose a “true” branch; otherwise the primary template yields “false.”How can I detect if a type is iterable?
Usevoid_t with decltype and std::declval to check for begin/end and a dereferenceable iterator: specialize when decltype(*begin(std::declval<C>())) and decltype(end(std::declval<C>())) are well-formed. Combine with if constexpr to select implementations safely.When should I use if constexpr instead of a normal if in templates?
Use if constexpr when the condition depends on compile-time information and only the selected branch needs to be instantiated. This avoids invalid code in the non-taken branch from causing errors (e.g., calling a function that exists only for certain types).What’s the difference between std::invoke and std::apply, and why do they matter?
- std::invoke uniformly calls any callable (function objects, function pointers, pointers to member functions/variables) with normal arguments.-
std::apply calls a callable using arguments stored in a std::tuple. It’s perfect when you accumulate arguments (e.g., in a curried function) and need to call the original function with them.How do I implement a generic currying utility, and what should I watch out for?
- Store the target callable and captured arguments by value in astd::tuple (use std::decay_t).- On each call, append new arguments; if
std::is_invocable_v with all captured args is true, std::apply the function; otherwise return a new curried object with the extended tuple.- Use
std::invoke/std::apply for universal call support.- Use
std::ref/std::cref if you must keep references to noncopyable or heavy objects.
Functional Programming in C++ ebook for free