Templates & Metaprogramming
Templates are C++βs mechanism for generic programming β writing code that works with any type. Metaprogramming takes it further: computing things at compile time.
Table of Contents
1 β Function Templates
2 β Class Templates
3 β Variadic Templates
4 β SFINAE
5 β Type Traits
6 β Compile-Time Computation
7 β CRTP
8 β Template Argument Deduction
9 β Common Template Patterns
Glossary β Key Terms at a Glance
| Term | Meaning |
|---|---|
| Template | Code parameterized by types and/or values β instantiated at compile time |
| Specialization | Override template behavior for specific types (full or partial) |
| Variadic Template | Template that accepts any number of type/value parameters |
| Fold Expression | C++17 syntax to reduce a parameter pack with an operator |
| SFINAE | Substitution Failure Is Not An Error β enables/disables overloads |
| Type Trait | Compile-time introspection of type properties |
| TMP | Template Metaprogramming β computing with types at compile time |
| CRTP | Curiously Recurring Template Pattern β static polymorphism |
| CTAD | Class Template Argument Deduction (C++17) |
if constexpr |
Compile-time branching that discards untaken branches |
1 β Function Templates
1.1 Basic Function Templates
Definition: A function template is a blueprint for a function parameterized by one or more types. The compiler generates concrete functions for each type used.
template<typename T>
T max_val(T a, T b) {
return (a > b) ? a : b;
}
max_val(3, 5); // T = int, returns 5
max_val(3.14, 2.71); // T = double, returns 3.14
max_val<int>(3.0, 5.0); // explicit: T = int, converts arguments
π Why This Logic: The compiler deduces
Tfrom the function arguments. Each uniqueTcreates a separate function instance β this is called template instantiation.
1.2 Multiple & Non-Type Parameters
Multiple Template Parameters:
template<typename T, typename U>
auto add(T a, U b) {
return a + b; // return type deduced (C++14)
}
add(1, 2.5); // T=int, U=double, returns double
Non-Type Template Parameters:
template<typename T, int N>
class FixedArray {
T data[N];
public:
T& operator[](int i) { return data[i]; }
constexpr int size() const { return N; }
};
FixedArray<double, 100> arr; // stack-allocated, size known at compile time
Tip: Non-type parameters must be compile-time constants. They enable stack allocation (no heap!) and allow the compiler to optimize based on known sizes.
2 β Class Templates
2.1 Class Templates & Specialization
Basic Class Template:
template<typename T>
class Stack {
std::vector<T> data;
public:
void push(const T& val) { data.push_back(val); }
void push(T&& val) { data.push_back(std::move(val)); }
T& top() { return data.back(); }
const T& top() const { return data.back(); }
void pop() { data.pop_back(); }
bool empty() const { return data.empty(); }
size_t size() const { return data.size(); }
};
Stack<int> intStack;
Stack<std::string> strStack;
Definition: Template specialization lets you override the default template behavior for specific types.
Full Specialization (one specific type):
// Primary template
template<typename T>
class Printer {
public:
void print(const T& val) { std::cout << val << "\n"; }
};
// Full specialization for bool
template<>
class Printer<bool> {
public:
void print(bool val) { std::cout << (val ? "true" : "false") << "\n"; }
};
Partial Specialization (a category of types):
// Partial specialization for pointers
template<typename T>
class Printer<T*> {
public:
void print(T* val) {
if (val) std::cout << *val << "\n";
else std::cout << "nullptr\n";
}
};
β οΈ Note: Function templates cannot be partially specialized β use overloading or if constexpr instead.
3 β Variadic Templates
3.1 Recursive Variadic Templates
Definition: Variadic templates accept any number of template arguments using a parameter pack (typename... Args).
// Base case
void print() {
std::cout << "\n";
}
// Recursive case
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // recursion with remaining args
}
print(1, "hello", 3.14, true); // "1 hello 3.14 1"
sizeof... β Count Arguments:
template<typename... Args>
void info(Args... args) {
std::cout << "Got " << sizeof...(Args) << " arguments\n";
}
3.2 Fold Expressions (C++17)
Fold expressions replace recursive variadic patterns with a single line:
// Left fold: ((a + b) + c) + d
template<typename... Args>
auto sum(Args... args) {
return (... + args);
}
// Right fold: a + (b + (c + d))
template<typename... Args>
auto sum_r(Args... args) {
return (args + ...);
}
// Fold with initial value
template<typename... Args>
auto sum_init(Args... args) {
return (0 + ... + args); // ((0 + a) + b) + c
}
// Print all with separator
template<typename... Args>
void printAll(Args... args) {
((std::cout << args << ", "), ...);
std::cout << "\n";
}
sum(1, 2, 3, 4); // 10
Why Fold Expressions? They eliminate the need for a base-case overload and recursive expansion. One line replaces 10+ lines of recursive template code.
4 β SFINAE
4.1 enable_if & Concepts
Definition: SFINAE (Substitution Failure Is Not An Error) β if template argument substitution fails, the compiler silently discards that overload instead of producing an error.
Classic SFINAE with enable_if:
#include <type_traits>
// Enable only for integral types
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
safe_divide(T a, T b) {
return b != 0 ? a / b : 0;
}
// C++14: cleaner with enable_if_t
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
safe_divide(T a, T b) {
return b != 0.0 ? a / b : 0.0;
}
C++20 Concepts replace SFINAE β vastly more readable:
template<std::integral T>
T safe_divide(T a, T b) { return b != 0 ? a / b : 0; }
template<std::floating_point T>
T safe_divide(T a, T b) { return b != 0.0 ? a / b : 0.0; }
π SFINAE errors produce 100-line error messages. Concepts produce: βT does not satisfy integralβ. Always prefer Concepts when using C++20.
5 β Type Traits
5.1 Built-in & Custom Type Traits
Built-in Type Traits (<type_traits>):
// Type queries
std::is_integral_v<int>; // true
std::is_floating_point_v<double>; // true
std::is_pointer_v<int*>; // true
std::is_const_v<const int>; // true
std::is_reference_v<int&>; // true
std::is_same_v<int, int32_t>; // true (on most platforms)
// Type transformations
std::remove_const_t<const int>; // int
std::remove_reference_t<int&>; // int
std::add_pointer_t<int>; // int*
std::decay_t<const int&>; // int
std::common_type_t<int, double>; // double
// Conditional type
std::conditional_t<true, int, double>; // int
std::conditional_t<false, int, double>; // double
Custom Type Traits:
// Check if type has a .size() method
template<typename T, typename = void>
struct has_size : std::false_type {};
template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
static_assert(has_size<std::vector<int>>::value); // true
static_assert(!has_size<int>::value); // true
π Why This Logic:
std::void_tis the SFINAE probe. IfT.size()is valid, the partial specialization matches and inheritstrue_type. If not, substitution fails and the primary template (inheritingfalse_type) is used.
6 β Compile-Time Computation
6.1 constexpr & TMP
constexpr Functions (preferred):
constexpr int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
constexpr int fib10 = fibonacci(10); // 55, computed at compile time
// constexpr with arrays (C++14)
constexpr auto makeTable() {
std::array<int, 256> table{};
for (int i = 0; i < 256; ++i) {
table[i] = i * i;
}
return table;
}
constexpr auto squares = makeTable(); // entire table built at compile time
Template Metaprogramming (TMP) β classic approach:
template<int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0> {
static constexpr int value = 1;
};
static_assert(Factorial<5>::value == 120);
Prefer constexpr functions over TMP for value computation. TMP is still useful for type computation (e.g., building type lists, conditional types).
6.2 if constexpr
Definition: if constexpr evaluates the condition at compile time. Branches that donβt match are not compiled β no SFINAE needed.
template<typename T>
std::string to_string_smart(T val) {
if constexpr (std::is_same_v<T, std::string>) {
return val;
} else if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(val);
} else {
return "unknown";
}
}
// Only the matching branch is compiled β no overhead, no errors in dead branches
7 β CRTP
7.1 Curiously Recurring Template Pattern
Definition: CRTP β a class inherits from a template parameterized by itself. This enables static (compile-time) polymorphism β no virtual function overhead.
template<typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
// Default implementation
void implementation() {
std::cout << "Default\n";
}
};
class Concrete : public Base<Concrete> {
public:
void implementation() {
std::cout << "Concrete\n";
}
};
Concrete c;
c.interface(); // "Concrete" β no vtable lookup!
Use case: High-performance systems where virtual function overhead matters (trading engines, game loops). CRTP eliminates the vtable indirection β the call is resolved at compile time and can be inlined.
8 β Template Argument Deduction
8.1 CTAD & Deduction Guides
Class Template Argument Deduction (CTAD, C++17):
std::pair p{1, 2.0}; // std::pair<int, double>
std::vector v{1, 2, 3}; // std::vector<int>
std::optional o{42}; // std::optional<int>
std::tuple t{1, "hi", 3.0}; // std::tuple<int, const char*, double>
Custom Deduction Guides:
template<typename T>
class MyContainer {
std::vector<T> data;
public:
MyContainer(std::initializer_list<T> list) : data(list) {}
};
// Deduction guide
template<typename T>
MyContainer(std::initializer_list<T>) -> MyContainer<T>;
MyContainer c{1, 2, 3}; // MyContainer<int>
9 β Common Template Patterns
9.1 Tag Dispatch & Policy-Based Design
Tag Dispatch β select implementation based on iterator category:
namespace detail {
template<typename Iter>
void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
it += n; // O(1)
}
template<typename Iter>
void advance_impl(Iter& it, int n, std::input_iterator_tag) {
for (int i = 0; i < n; ++i) ++it; // O(n)
}
}
template<typename Iter>
void advance(Iter& it, int n) {
detail::advance_impl(it, n,
typename std::iterator_traits<Iter>::iterator_category{});
}
Policy-Based Design β compose behavior via template parameters:
template<typename SortPolicy, typename PrintPolicy>
class DataProcessor {
SortPolicy sorter;
PrintPolicy printer;
public:
void process(std::vector<int>& data) {
sorter.sort(data);
printer.print(data);
}
};
struct QuickSort {
void sort(std::vector<int>& v) { std::sort(v.begin(), v.end()); }
};
struct ConsolePrint {
void print(const std::vector<int>& v) {
for (auto x : v) std::cout << x << " ";
}
};
DataProcessor<QuickSort, ConsolePrint> dp;
π Why Policy-Based Design? It gives you the flexibility of runtime polymorphism (swap algorithms) with zero overhead β everything is resolved at compile time.
Practice Questions
Q1. Write a function template min_of that works with any type supporting <. What happens if you call it with two different types?
Q2. Explain the difference between full specialization and partial specialization. Give an example of each.
Q3. Write a variadic template count_args that returns the number of arguments at compile time (as a constexpr).
Q4. Rewrite a recursive variadic product(args...) using C++17 fold expressions. Show both versions.
Q5. Explain SFINAE with a concrete example. Then rewrite the same logic with C++20 Concepts.
Q6. Create a custom type trait is_iterable<T> that checks if a type has begin() and end().
Q7. Write a constexpr function to check if a number is prime. Verify it works at compile time using static_assert.
Q8. Explain CRTP. Why does it outperform virtual functions? What is a disadvantage?
Q9. What is CTAD? Write a deduction guide for a custom Wrapper<T> class.
Q10. Compare tag dispatch vs if constexpr vs Concepts for selecting different implementations. When would you use each?
Key Takeaways
- Templates = generic programming β write once, works for any type
- Prefer
constexprover template metaprogramming for value computation - Use
if constexprinstead of SFINAE when possible (C++17) - Use Concepts instead of SFINAE (C++20) β vastly more readable
- CRTP for compile-time polymorphism in performance-critical code
- Specialization lets you customize behavior for specific types
- Fold expressions (C++17) eliminate recursive variadic template patterns