Template Metaprogramming (TMP) in C++ is a technique that uses the compiler’s template instantiation mechanism to perform computations at compile time rather than at runtime. By writing templates that recursively instantiate themselves with modified parameters, you can compute values, manipulate types, generate specialized code, and enforce constraints — all before your program even runs. The results are zero-runtime-overhead abstractions that would otherwise require expensive runtime computation or dynamic dispatch.
Introduction
When most programmers think about C++, they think about runtime behavior: objects being created, functions being called, data being processed as the program executes. But C++ has a second, parallel universe that operates entirely at compile time — a functional programming language embedded inside the type system, powered by templates.
This is Template Metaprogramming (TMP). The “meta” in metaprogramming means writing programs that reason about programs — in this case, writing templates that the C++ compiler evaluates during compilation to produce specialized code, computed constants, and type information that would otherwise require runtime work.
TMP was discovered almost accidentally in the early days of C++ when programmers noticed that template instantiation was itself a Turing-complete computation system. Andrei Alexandrescu’s book Modern C++ Design and the Boost libraries popularized TMP techniques, and C++11 through C++20 have progressively made compile-time programming more powerful and readable.
Understanding TMP gives you access to capabilities that are simply impossible in most other languages: types that change behavior based on the properties of their template arguments, compile-time-verified constraints on template parameters, algorithms that generate different machine code for different input types with zero branching, and library interfaces that adapt themselves to whatever types you use with them.
This article walks you through TMP from its foundations. You will understand compile-time values and types, recursive template patterns, type traits, constexpr functions, and the modern C++17/C++20 facilities that make TMP readable and practical. Every concept is illustrated with runnable code and thorough explanations.
The Foundation: Templates as Compile-Time Functions
The core insight of TMP is that template instantiation is a form of function application — it takes parameters (types or values) and produces a result (a type, a value, or more code). When you write:
template<int N>
struct Square {
static constexpr int value = N * N;
};You have created a “compile-time function” that maps an integer to its square. Calling it looks like Square<7>::value, and the compiler evaluates it entirely during compilation — no runtime computation occurs.
#include <iostream>
using namespace std;
// Compile-time square: computed at compile time
template<int N>
struct Square {
static constexpr int value = N * N;
};
// Compile-time absolute value
template<int N>
struct Abs {
static constexpr int value = (N < 0) ? -N : N;
};
// Compile-time max of two values
template<int A, int B>
struct Max {
static constexpr int value = (A > B) ? A : B;
};
// Compile-time min of two values
template<int A, int B>
struct Min {
static constexpr int value = (A < B) ? A : B;
};
int main() {
// All values computed entirely at compile time
constexpr int sq7 = Square<7>::value; // 49
constexpr int absN = Abs<-15>::value; // 15
constexpr int maxAB = Max<42, 17>::value; // 42
constexpr int minAB = Min<42, 17>::value; // 17
cout << "Square<7> = " << sq7 << endl;
cout << "Abs<-15> = " << absN << endl;
cout << "Max<42,17> = " << maxAB << endl;
cout << "Min<42,17> = " << minAB << endl;
// Use in static array size: compiler-computed dimension
int grid[Max<3, 5>::value][Max<3, 5>::value]; // 5x5 grid
cout << "Grid size: " << sizeof(grid) / sizeof(int) << " elements" << endl;
return 0;
}Output:
Square<7> = 49
Abs<-15> = 15
Max<42,17> = 42
Min<42,17> = 17
Grid size: 25 elementsStep-by-step explanation:
Square<7>::valueis not a function call — it is a member access on a class template instantiation. The compiler instantiatesSquarewithN = 7, computes7 * 7 = 49, and stores it asstatic constexpr int value. All of this happens during compilation.constexpr int sq7 = Square<7>::valuestores the result in a compile-time constant. You can verify this is truly compile-time by using it where only compile-time values are allowed — like array dimensions (int grid[Max<3,5>::value][Max<3,5>::value]). If it required runtime evaluation, this line would be a compile error.- Template structs acting as compile-time functions are the historical foundation of TMP. They are often called metafunctions — functions that operate on types and compile-time values rather than runtime values.
- The
static constexprcombination means: one value per template instantiation (static), available at compile time (constexpr). EverySquare<N>instantiation has its ownvalue, computed for that specificN.
Recursive TMP: Computing Fibonacci and Factorial
The most powerful TMP patterns involve recursive template instantiation: a template that instantiates itself with modified parameters, with a base case that terminates the recursion. This mirrors recursive functions but operates entirely at compile time.
Compile-Time Factorial
#include <iostream>
using namespace std;
// Recursive case: N! = N * (N-1)!
template<unsigned int N>
struct Factorial {
static constexpr unsigned long long value = N * Factorial<N-1>::value;
};
// Base case: 0! = 1
template<>
struct Factorial<0> {
static constexpr unsigned long long value = 1;
};
int main() {
cout << "0! = " << Factorial<0>::value << endl;
cout << "1! = " << Factorial<1>::value << endl;
cout << "5! = " << Factorial<5>::value << endl;
cout << "10! = " << Factorial<10>::value << endl;
cout << "12! = " << Factorial<12>::value << endl;
// All values are compile-time constants:
static_assert(Factorial<5>::value == 120, "5! must be 120");
static_assert(Factorial<10>::value == 3628800, "10! must be 3628800");
cout << "All static_assert checks passed at compile time!" << endl;
return 0;
}Output:
0! = 1
1! = 1
5! = 120
10! = 3628800
12! = 479001600
All static_assert checks passed at compile time!Step-by-step explanation:
Factorial<5>::valuecauses the compiler to instantiate:Factorial<5>→Factorial<4>→Factorial<3>→Factorial<2>→Factorial<1>→Factorial<0>. Each instantiation references the next, building a chain.Factorial<0>is a template specialization — a specific implementation for a particular template argument value (N = 0). It provides the base case that stops the recursion. Without it, the compiler would try to instantiateFactorial<-1>,Factorial<-2>, and so on forever, eventually hitting the template instantiation depth limit and generating a compile error.static_assert(Factorial<5>::value == 120, "...")is a compile-time assertion. If the condition is false, the program fails to compile with the provided error message. This is TMP’s “unit testing” — verifying correctness at compile time, not runtime.- The compiler generates zero assembly code for these computations. The value
120forFactorial<5>::valueis simply embedded as a literal constant in the compiled binary, just like writing120directly.
Compile-Time Fibonacci
#include <iostream>
using namespace std;
// Recursive case: fib(n) = fib(n-1) + fib(n-2)
template<unsigned int N>
struct Fibonacci {
static constexpr unsigned long long value =
Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
// Base cases
template<> struct Fibonacci<0> { static constexpr unsigned long long value = 0; };
template<> struct Fibonacci<1> { static constexpr unsigned long long value = 1; };
int main() {
cout << "Fibonacci sequence (compile-time):" << endl;
cout << "F(0) = " << Fibonacci<0>::value << endl;
cout << "F(1) = " << Fibonacci<1>::value << endl;
cout << "F(10) = " << Fibonacci<10>::value << endl;
cout << "F(20) = " << Fibonacci<20>::value << endl;
cout << "F(30) = " << Fibonacci<30>::value << endl;
// Important: compile-time Fibonacci is NOT exponential at runtime
// The compiler memoizes each instantiation — each Fibonacci<N> is
// computed once and reused wherever it is referenced
static_assert(Fibonacci<10>::value == 55, "F(10) must be 55");
return 0;
}Output:
Fibonacci sequence (compile-time):
F(0) = 0
F(1) = 1
F(10) = 55
F(20) = 6765
F(30) = 832040Step-by-step explanation:
Fibonacci<5>needsFibonacci<4>andFibonacci<3>.Fibonacci<4>needsFibonacci<3>andFibonacci<2>. Crucially, the compiler instantiates eachFibonacci<N>at most once and caches the result — soFibonacci<3>is computed once and reused, not recomputed exponentially as a naive runtime recursive Fibonacci would be.- Two base case specializations are needed:
Fibonacci<0>= 0 andFibonacci<1>= 1. Together they stop the recursion. - Notice that computing
Fibonacci<30>at compile time generates zero runtime overhead — the value832040is simply a literal constant in the binary. At runtime, this is as fast as reading a hardcoded number.
Type-Level TMP: Manipulating Types at Compile Time
TMP is not limited to computing integer values. Its most powerful applications involve computing with types — transforming one type into another, selecting between types based on conditions, and extracting information about types.
The Conditional Type: if-else at the Type Level
#include <iostream>
#include <type_traits>
using namespace std;
// Compile-time if: select TypeIfTrue if Condition is true, else TypeIfFalse
template<bool Condition, typename TypeIfTrue, typename TypeIfFalse>
struct Conditional {
using type = TypeIfTrue;
};
// Partial specialization for Condition == false
template<typename TypeIfTrue, typename TypeIfFalse>
struct Conditional<false, TypeIfTrue, TypeIfFalse> {
using type = TypeIfFalse;
};
// Helper alias (C++11 style)
template<bool Cond, typename T, typename F>
using Conditional_t = typename Conditional<Cond, T, F>::type;
int main() {
// Select int or double based on a compile-time bool
using SmallType = Conditional_t<true, int, double>;
using LargeType = Conditional_t<false, int, double>;
using StringType = Conditional_t<(sizeof(int) < sizeof(long long)), string, wstring>;
cout << "SmallType is int? " << is_same_v<SmallType, int> << endl;
cout << "LargeType is double? " << is_same_v<LargeType, double> << endl;
cout << "StringType is string? " << is_same_v<StringType, string> << endl;
// Practical use: choose storage type based on size requirement
template<size_t Size>
struct SmallOrLarge {
using StorageType = Conditional_t<(Size <= 4), int, long long>;
StorageType data;
};
SmallOrLarge<3> small; // Uses int storage
SmallOrLarge<8> large; // Uses long long storage
cout << "Small storage size: " << sizeof(small.data) << " bytes" << endl;
cout << "Large storage size: " << sizeof(large.data) << " bytes" << endl;
return 0;
}This pattern — Conditional<Condition, T, F>::type — is exactly what std::conditional<Condition, T, F>::type (and std::conditional_t<...>) does in the standard library. Let’s see it in action with a real use case:
#include <iostream>
#include <type_traits>
using namespace std;
// A container that uses int for small counts and long long for large ones
template<size_t MaxElements>
class CompactContainer {
public:
// Use int if max elements fits in int, otherwise use long long
using CountType = conditional_t<(MaxElements <= 1000000), int, long long>;
CountType count = 0;
void add() { ++count; }
void showInfo() {
cout << "CountType size: " << sizeof(CountType) << " bytes, "
<< "count = " << count << endl;
}
};
int main() {
CompactContainer<1000> small; // CountType = int (4 bytes)
CompactContainer<5000000> large; // CountType = long long (8 bytes)
small.add(); small.add(); small.add();
large.add();
small.showInfo(); // 4 bytes
large.showInfo(); // 8 bytes
return 0;
}Output:
CountType size: 4 bytes, count = 3
CountType size: 8 bytes, count = 1Step-by-step explanation:
conditional_t<(MaxElements <= 1000000), int, long long>selectsintwhenMaxElementsis at most 1,000,000 andlong longotherwise. This is evaluated entirely at compile time whenCompactContaineris instantiated.- For
CompactContainer<1000>,CountTypebecomesint— saving 4 bytes per instance. ForCompactContainer<5000000>,CountTypebecomeslong long— providing the necessary range. - There is no runtime branch. Each instantiation of
CompactContainerproduces a different, optimally-sized class. The compiler generates separate machine code for each instantiation.
Type Traits: Querying Type Properties at Compile Time
Type traits are compile-time predicates and transformations on types. The <type_traits> header provides dozens of them. Understanding how to build and use type traits is central to practical TMP.
#include <iostream>
#include <type_traits>
using namespace std;
// Building a custom type trait: is_pointer_to_const<T>
// True if T is a pointer to a const type (e.g., const int*)
template<typename T>
struct is_pointer_to_const : false_type {};
template<typename T>
struct is_pointer_to_const<const T*> : true_type {};
// A type trait that removes all qualifiers layer by layer
template<typename T>
struct deep_remove_cv {
using type = remove_cv_t<T>;
};
template<typename T>
struct deep_remove_cv<T*> {
using type = typename deep_remove_cv<T>::type*;
};
template<typename T>
using deep_remove_cv_t = typename deep_remove_cv<T>::type;
// Testing standard type traits
void demonstrateTypeTraits() {
// is_integral: is T an integer type?
cout << "is_integral<int>: " << is_integral_v<int> << endl; // 1
cout << "is_integral<float>: " << is_integral_v<float> << endl; // 0
cout << "is_integral<bool>: " << is_integral_v<bool> << endl; // 1
// is_floating_point
cout << "is_floating_point<double>: " << is_floating_point_v<double> << endl;
// is_pointer
cout << "is_pointer<int*>: " << is_pointer_v<int*> << endl; // 1
cout << "is_pointer<int>: " << is_pointer_v<int> << endl; // 0
// is_same: are two types identical?
cout << "is_same<int,int>: " << is_same_v<int, int> << endl; // 1
cout << "is_same<int,long>: " << is_same_v<int, long> << endl; // 0
// is_base_of: inheritance check
struct Base {};
struct Derived : Base {};
cout << "is_base_of<Base,Derived>: " << is_base_of_v<Base, Derived> << endl; // 1
cout << "is_base_of<Derived,Base>: " << is_base_of_v<Derived, Base> << endl; // 0
// Type transformations
using ConstInt = add_const_t<int>; // const int
using RawInt = remove_const_t<const int>; // int
using PtrInt = add_pointer_t<int>; // int*
using BaseInt = remove_pointer_t<int*>; // int
using NoRef = remove_reference_t<int&>; // int
using NoRefNoCV = decay_t<const int&>; // int
cout << "add_const<int> is const int? " << is_same_v<ConstInt, const int> << endl;
cout << "decay<const int&> is int? " << is_same_v<NoRefNoCV, int> << endl;
}
int main() {
demonstrateTypeTraits();
// Custom trait
cout << "\nCustom trait tests:" << endl;
cout << "is_pointer_to_const<const int*>: "
<< is_pointer_to_const<const int*>::value << endl; // 1
cout << "is_pointer_to_const<int*>: "
<< is_pointer_to_const<int*>::value << endl; // 0
cout << "is_pointer_to_const<const int**>: "
<< is_pointer_to_const<const int**>::value << endl; // 0 (outer ptr not to const)
return 0;
}Output:
is_integral<int>: 1
is_integral<float>: 0
is_integral<bool>: 1
is_floating_point<double>: 1
is_pointer<int*>: 1
is_pointer<int>: 0
is_same<int,int>: 1
is_same<int,long>: 0
is_base_of<Base,Derived>: 1
is_base_of<Derived,Base>: 0
add_const<int> is const int? 1
decay<const int&> is int? 1
Custom trait tests:
is_pointer_to_const<const int*>: 1
is_pointer_to_const<int*>: 0
is_pointer_to_const<const int**>: 0Step-by-step explanation:
- Standard type traits like
is_integral_v<T>,is_pointer_v<T>, andis_same_v<T, U>are compile-time boolean predicates. The_vsuffix is a C++17 variable template shorthand for::value. - Type transformation traits like
add_const_t<T>,remove_reference_t<T>, anddecay_t<T>compute new types from existing ones. The_tsuffix is a C++14 alias template shorthand for::type. decay_t<const int&>removes the reference and cv-qualifiers, producingint. This is what happens to function arguments when passed by value —decaymodels that transformation.- The custom
is_pointer_to_const<T>trait uses partial template specialization: the primary template returnsfalse_typefor allT, and the specialization forconst T*returnstrue_type. This is the standard pattern for writing type traits. true_typeandfalse_typeare standard library types that inherit fromintegral_constant<bool, true>andintegral_constant<bool, false>respectively. They provide::value,operator bool(), and other conveniences for use in TMP.
if constexpr: Modern Compile-Time Branching
C++17 introduced if constexpr, which is the modern, readable replacement for many recursive TMP patterns. Unlike a regular if, if constexpr discards the non-taken branch at compile time — meaning the discarded branch is not instantiated, not type-checked, and generates no code.
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>
using namespace std;
// Universal toString: handles any type appropriately
template<typename T>
string toString(const T& value) {
if constexpr (is_same_v<T, string>) {
return value; // Already a string
} else if constexpr (is_arithmetic_v<T>) {
return to_string(value); // Numbers
} else if constexpr (is_same_v<T, bool>) {
return value ? "true" : "false";
} else {
// For anything with operator<<, use stringstream
ostringstream oss;
oss << value;
return oss.str();
}
}
// Process function: different behavior for arithmetic vs. other types
template<typename T>
void process(const T& value) {
if constexpr (is_arithmetic_v<T>) {
cout << "Arithmetic: " << value << " * 2 = " << (value * 2) << endl;
// This branch compiles only for arithmetic T
// value * 2 would be a compile error for string — but it's never compiled!
} else if constexpr (is_same_v<T, string>) {
cout << "String: '" << value << "', length = " << value.length() << endl;
// value.length() would be a compile error for int — but discarded
} else {
cout << "Other type: " << value << endl;
}
}
// Recursively sum a variadic pack
template<typename T>
T sumAll(T value) { return value; }
template<typename T, typename... Rest>
T sumAll(T first, Rest... rest) {
if constexpr (sizeof...(rest) == 0) {
return first;
} else {
return first + sumAll(rest...);
}
}
int main() {
process(42);
process(3.14);
process(string("hello"));
cout << "\ntoString tests:" << endl;
cout << toString(42) << endl;
cout << toString(3.14f) << endl;
cout << toString(string("world")) << endl;
cout << "\nsumAll(1,2,3,4,5) = " << sumAll(1, 2, 3, 4, 5) << endl;
cout << "sumAll(1.5,2.5) = " << sumAll(1.5, 2.5) << endl;
return 0;
}Output:
Arithmetic: 42 * 2 = 84
Arithmetic: 3.14 * 2 = 6.28
String: 'hello', length = 5
toString tests:
42
3.14
world
sumAll(1,2,3,4,5) = 15
sumAll(1.5,2.5) = 3Step-by-step explanation:
if constexpr (is_arithmetic_v<T>)evaluates the condition at compile time using the type trait. IfTisint, the arithmetic branch is compiled and the string branch is completely discarded — it is never instantiated.- This means
value * 2is only compiled whenTis arithmetic. IfT = string,value * 2would be a compile error — but since theif constexprbranch is discarded for non-arithmetic types, it never causes an error. - Before
if constexpr, achieving this required multiple function template overloads or complex SFINAE expressions.if constexprmakes type-conditional behavior readable and maintainable. - In
sumAll,if constexpr (sizeof...(rest) == 0)checks at compile time whether the rest of the pack is empty. This eliminates the need for the separate base-case overload that recursive variadic templates traditionally require.
Compile-Time String Manipulation with constexpr
constexpr functions (introduced in C++11, greatly expanded in C++14 and C++17) are a modern, readable alternative to recursive template structs for computing values. They look like regular functions but are evaluated at compile time when called with constant expressions.
#include <iostream>
#include <array>
using namespace std;
// Compile-time string length (counts chars until null terminator)
constexpr size_t ctstrlen(const char* s) {
size_t len = 0;
while (s[len] != '\0') ++len;
return len;
}
// Compile-time FNV-1a hash of a string
constexpr uint32_t fnvHash(const char* s, size_t len) {
uint32_t hash = 2166136261u;
for (size_t i = 0; i < len; ++i) {
hash ^= static_cast<uint32_t>(s[i]);
hash *= 16777619u;
}
return hash;
}
// Convenient overload: compute hash from string literal directly
constexpr uint32_t operator""_hash(const char* s, size_t len) {
return fnvHash(s, len);
}
// Compile-time power function: base^exp
constexpr long long power(long long base, unsigned int exp) {
if (exp == 0) return 1;
if (exp % 2 == 0) {
long long half = power(base, exp / 2);
return half * half;
}
return base * power(base, exp - 1);
}
// Compile-time check if a number is prime
constexpr bool isPrime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
int main() {
// String operations at compile time
constexpr size_t len = ctstrlen("Hello, World!");
cout << "Length of \"Hello, World!\": " << len << endl;
static_assert(len == 13, "Length must be 13");
// Compile-time hashing — useful for switch statements on strings
constexpr uint32_t h1 = "GET"_hash;
constexpr uint32_t h2 = "POST"_hash;
constexpr uint32_t h3 = "DELETE"_hash;
cout << "Hash of \"GET\": " << h1 << endl;
cout << "Hash of \"POST\": " << h2 << endl;
cout << "Hash of \"DELETE\": " << h3 << endl;
// Switching on string hashes (constant expressions in switch)
const char* method = "POST";
switch (fnvHash(method, ctstrlen(method))) {
case "GET"_hash: cout << "Handling GET request" << endl; break;
case "POST"_hash: cout << "Handling POST request" << endl; break;
case "DELETE"_hash: cout << "Handling DELETE request" << endl; break;
default: cout << "Unknown method" << endl;
}
// Compile-time power
constexpr long long p = power(2, 10); // 1024
cout << "2^10 = " << p << endl;
static_assert(p == 1024, "2^10 must be 1024");
// Compile-time prime check — generate primes up to 30
cout << "Primes up to 30: ";
for (int i = 2; i <= 30; ++i) {
if constexpr (false) {} // placeholder
// Using runtime loop with constexpr function — still runtime here
}
// Use static way: template with constexpr
constexpr int primes[] = {
isPrime(2) ? 2 : 0,
isPrime(3) ? 3 : 0,
isPrime(5) ? 5 : 0,
isPrime(7) ? 7 : 0,
isPrime(11) ? 11 : 0,
isPrime(13) ? 13 : 0,
isPrime(17) ? 17 : 0,
isPrime(19) ? 19 : 0,
isPrime(23) ? 23 : 0,
isPrime(29) ? 29 : 0
};
for (int p : primes) if (p) cout << p << " ";
cout << endl;
return 0;
}Output:
Length of "Hello, World!": 13
Hash of "GET": 2087226743
Hash of "POST": 2614700765
Hash of "DELETE": 2535029701
Handling POST request
2^10 = 1024
Primes up to 30: 2 3 5 7 11 13 17 19 23 29 Step-by-step explanation:
constexpr size_t len = ctstrlen("Hello, World!")computes the string length at compile time. Thewhileloop insidectstrlenis evaluated by the compiler — no loop runs at runtime.- The user-defined literal
operator""_hashallows the syntax"GET"_hashto compute a hash of a string literal at compile time. Used inswitchcases, these become compile-time integer constants, enabling constant-time dispatch on strings. constexpr long long p = power(2, 10)computes2^10 = 1024at compile time.static_assert(p == 1024)verifies this is correct — if the function were wrong, the program would fail to compile.isPrime(n)is aconstexprfunction used to populate a compile-time array. Every element is computed during compilation. The resultingprimes[]array contains no runtime computation — just stored constants.constexprfunctions can be called both at compile time (when arguments are constant expressions) and at runtime (when called with runtime values). A single implementation serves both purposes.
TMP Pattern: Policy-Based Design
One of the most practical TMP patterns is policy-based design, where behavior is selected at compile time by passing a “policy” type as a template parameter. This gives compile-time polymorphism with zero virtual dispatch overhead.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// Sorting policies — different algorithms as type parameters
struct BubbleSort {
static const char* name() { return "BubbleSort"; }
template<typename It>
static void sort(It begin, It end) {
for (auto i = begin; i != end; ++i)
for (auto j = begin; j < i; ++j)
if (*i < *j) std::swap(*i, *j);
}
};
struct StdSort {
static const char* name() { return "StdSort"; }
template<typename It>
static void sort(It begin, It end) {
std::sort(begin, end);
}
};
struct InsertionSort {
static const char* name() { return "InsertionSort"; }
template<typename It>
static void sort(It begin, It end) {
for (auto i = begin + 1; i != end; ++i) {
auto key = *i;
auto j = i - 1;
while (j >= begin && *j > key) {
*(j + 1) = *j;
--j;
}
*(j + 1) = key;
}
}
};
// Generic sorter: policy selected at compile time
template<typename SortPolicy>
class Sorter {
public:
template<typename Container>
void sort(Container& c) {
cout << "Using " << SortPolicy::name() << endl;
SortPolicy::sort(c.begin(), c.end());
}
};
// Logging policy
struct ConsoleLogger {
static void log(const string& msg) { cout << "[LOG] " << msg << endl; }
};
struct SilentLogger {
static void log(const string& /*msg*/) {} // No-op at compile time
};
// Data processor with pluggable sort and log policies
template<typename SortPolicy, typename LogPolicy = ConsoleLogger>
class DataProcessor {
vector<int> data_;
public:
void addData(initializer_list<int> values) {
data_.insert(data_.end(), values.begin(), values.end());
LogPolicy::log("Added " + to_string(values.size()) + " items");
}
void process() {
LogPolicy::log("Sorting " + to_string(data_.size()) + " items");
SortPolicy::sort(data_.begin(), data_.end());
LogPolicy::log("Sort complete");
}
void print() const {
for (int v : data_) cout << v << " ";
cout << endl;
}
};
int main() {
cout << "=== Policy-based sorters ===" << endl;
Sorter<BubbleSort> bubbleSorter;
Sorter<StdSort> stdSorter;
Sorter<InsertionSort> insertSorter;
vector<int> v1 = {5, 3, 8, 1, 9, 2};
vector<int> v2 = v1, v3 = v1;
bubbleSorter.sort(v1); for (int x : v1) cout << x << " "; cout << endl;
stdSorter.sort(v2); for (int x : v2) cout << x << " "; cout << endl;
insertSorter.sort(v3); for (int x : v3) cout << x << " "; cout << endl;
cout << "\n=== Data processor with policies ===" << endl;
// Verbose processor: BubbleSort + ConsoleLogger
DataProcessor<BubbleSort, ConsoleLogger> verboseProc;
verboseProc.addData({7, 2, 5, 1, 8});
verboseProc.process();
verboseProc.print();
cout << endl;
// Silent processor: StdSort + SilentLogger (no log output)
DataProcessor<StdSort, SilentLogger> silentProc;
silentProc.addData({7, 2, 5, 1, 8});
silentProc.process();
silentProc.print();
return 0;
}Output:
=== Policy-based sorters ===
Using BubbleSort
1 2 3 5 8 9
Using StdSort
1 2 3 5 8 9
Using InsertionSort
1 2 3 5 8 9
=== Data processor with policies ===
[LOG] Added 5 items
[LOG] Sorting 5 items
Using BubbleSort
[LOG] Sort complete
1 2 5 7 8
1 2 5 7 8 Step-by-step explanation:
SortPolicy::sort(begin, end)calls the sort algorithm specified by the template parameter. ForSorter<BubbleSort>, this is bubble sort. ForSorter<StdSort>, this isstd::sort. There is no virtual dispatch — the compiler knows exactly which function to call at compile time.LogPolicy::log(msg)calls the log function. ForSilentLogger, this is a no-op function that the compiler will completely optimize away — no string construction, no output call, truly zero overhead logging in release builds.DataProcessor<StdSort, SilentLogger>produces a class where the logging code is entirely absent from the generated machine code. Contrast with runtime polymorphism (virtual functions), where even a no-op virtual call has function pointer overhead.- This pattern is used extensively in the C++ standard library itself: allocators as template parameters, comparators as template parameters, and hash functions as template parameters are all policy-based designs.
Compile-Time Lookup Tables
A compelling practical application of TMP is generating lookup tables entirely at compile time, embedding them directly in the binary as static data.
#include <iostream>
#include <array>
using namespace std;
// Generate a compile-time sine table (approximated with integer arithmetic)
// For a real implementation you'd use constexpr math functions
constexpr double ct_pi = 3.14159265358979323846;
constexpr double ct_sin(double x) {
// Taylor series: sin(x) = x - x^3/6 + x^5/120 - x^7/5040 + ...
double result = x;
double term = x;
double xSq = x * x;
int sign = -1;
for (int i = 1; i <= 10; ++i) {
term *= xSq / ((2*i) * (2*i + 1));
result += sign * term;
sign = -sign;
}
return result;
}
// Generate a compile-time lookup table of N sine values
template<size_t N>
constexpr array<double, N> makeSineTable() {
array<double, N> table{};
for (size_t i = 0; i < N; ++i) {
double angle = (2.0 * ct_pi * i) / N;
table[i] = ct_sin(angle);
}
return table;
}
// Compile-time CRC8 table for fast CRC computation
constexpr array<uint8_t, 256> makeCRC8Table() {
array<uint8_t, 256> table{};
for (int i = 0; i < 256; ++i) {
uint8_t crc = static_cast<uint8_t>(i);
for (int j = 0; j < 8; ++j) {
crc = (crc & 0x80) ? ((crc << 1) ^ 0x07) : (crc << 1);
}
table[i] = crc;
}
return table;
}
// Use the CRC8 table for fast checksum computation
uint8_t computeCRC8(const uint8_t* data, size_t len) {
static constexpr auto table = makeCRC8Table(); // Generated at compile time
uint8_t crc = 0;
for (size_t i = 0; i < len; ++i) {
crc = table[crc ^ data[i]];
}
return crc;
}
int main() {
// Sine table: 8 values, computed at compile time
constexpr auto sineTable = makeSineTable<8>();
cout << "Compile-time sine table (8 points):" << endl;
for (size_t i = 0; i < 8; ++i) {
cout << " sin(2π * " << i << "/8) = " << sineTable[i] << endl;
}
// CRC8 table: 256 values, computed at compile time
const uint8_t message[] = {0x31, 0x32, 0x33, 0x34, 0x35};
uint8_t crc = computeCRC8(message, sizeof(message));
cout << "\nCRC8 of message: 0x" << hex << (int)crc << endl;
return 0;
}Output:
Compile-time sine table (8 points):
sin(2π * 0/8) = 0
sin(2π * 1/8) = 0.707107
sin(2π * 2/8) = 1
sin(2π * 3/8) = 0.707107
sin(2π * 4/8) = -2.44929e-16
sin(2π * 5/8) = -0.707107
sin(2π * 6/8) = -1
sin(2π * 7/8) = -0.707107
CRC8 of message: 0x31Step-by-step explanation:
makeSineTable<8>()is aconstexprfunction that computes 8 sine values at compile time using a Taylor series approximation. The resultingarray<double, 8>is a compile-time constant embedded directly in the binary’s read-only data segment.makeCRC8Table()generates all 256 entries of a CRC8 lookup table at compile time. This table is then used incomputeCRC8for fast runtime CRC computation — each byte processed with a single array lookup instead of a polynomial division.static constexpr auto table = makeCRC8Table()insidecomputeCRC8ensures the table is computed once at compile time and stored as static data. Each call tocomputeCRC8uses the pre-computed table — zero recomputation overhead.- Compile-time table generation is a major pattern in embedded and performance-critical software: sin/cos tables for audio DSP, CRC tables for data integrity, hash tables for string matching, and more.
TMP and Modern C++: The Evolution
The history of TMP reflects the evolution of C++ toward making compile-time programming more readable and less arcane:
| Era | Technique | Example |
|---|---|---|
| C++98/03 | Recursive template structs with specializations | Factorial<N> with Factorial<0> specialization |
| C++11 | constexpr functions, type_traits, static_assert | constexpr int f(int n) + static_assert |
| C++11 | Variadic templates, template aliases (using) | template<typename... Ts> |
| C++14 | constexpr relaxed (loops, local vars), _t and _v aliases | if in constexpr, is_same_v<T,U> |
| C++17 | if constexpr, fold expressions, constexpr if | if constexpr (is_integral_v<T>) |
| C++20 | consteval, constinit, Concepts, constexpr containers | concept Sortable = ... |
The trend is clear: TMP power has not decreased, but the syntax required has become dramatically more readable. What once required three template specializations and a typedef can now be written as a readable if constexpr branch.
Practical Guidelines: When to Use TMP
TMP is powerful, but it comes with real costs in compilation time, error message readability, and code maintainability. Use it purposefully.
Use TMP when:
- You need compile-time constants for array sizes, template arguments, or static assertions.
- You want type-level polymorphism without virtual dispatch overhead (policy-based design).
- You are writing library code that must work correctly and efficiently with any type that meets certain requirements.
- You are generating lookup tables, bit patterns, or other data that is fully determined at compile time.
- You want to enforce constraints on template parameters (type traits +
static_assertor Concepts).
Prefer runtime solutions when:
- The value is not known until runtime.
- Code readability and maintainability are more important than compile-time overhead.
- Compilation time is already a bottleneck (heavy TMP significantly increases compile times).
- The performance gain is not measurable or meaningful for your use case.
Modern best practices: Use constexpr functions instead of recursive template structs whenever possible — they are far more readable and debuggable. Use if constexpr instead of SFINAE for type dispatch. Use Concepts (C++20) instead of enable_if for constraining templates. Reserve complex recursive template metaprogramming for cases where constexpr genuinely cannot express the required type-level computation.
Conclusion
Template Metaprogramming transforms C++ templates from a simple code-reuse mechanism into a complete compile-time computation system. By performing computations during compilation rather than at runtime, TMP produces zero-overhead abstractions: compile-time constants, type-selected algorithms, generated lookup tables, and policy-based designs that adapt to their template arguments without any runtime cost.
The foundations are straightforward: template structs as compile-time functions, template specializations as base cases, recursive instantiation as iteration. These patterns compute factorials, Fibonacci numbers, type transformations, and arbitrary values before your program ever runs.
Modern C++ has made TMP dramatically more approachable. constexpr functions replace recursive template structs for value computation. if constexpr replaces SFINAE for conditional compilation. Type traits provide ready-made compile-time predicates. And C++20 Concepts provide a clean, readable syntax for constraining template parameters.
Mastering TMP means mastering the full power of C++ as a language — not just its runtime behavior, but its compile-time behavior as well. The ability to move computation from runtime to compile time, to select algorithms based on types, and to enforce correctness through the type system rather than through runtime checks is what makes C++ uniquely powerful among mainstream programming languages.








