Skip to content
Home
C++ Templates: Advanced Techniques and Patterns

C++ Templates: Advanced Techniques and Patterns

C/C++ C/C++ 8 min read 1611 words Beginner ExcellentWiki Editorial Team

Templates are C++’s mechanism for generic programming. They let you write code that works with any type, but their power goes far beyond simple container classes like std::vector<T>. Advanced template techniques enable compile-time computation, type introspection, policy-based design, and even embedded domain-specific languages.

This guide covers template specialization, variadic templates, SFINAE, C++20 concepts, and template metaprogramming with practical examples.

Template Basics Refresher

template <typename T>
T max_of(T a, T b) {
    return (a > b) ? a : b;
---

template <typename T>
class Box {
    T value;
public:
    explicit Box(T v) : value(v) {}
    T get() const { return value; }
---;

Template Specialization

Full Specialization

Specialize a template for a specific type:

template <typename T>
struct TypeInfo {
    static const char* name() { return "unknown"; }
---;

template <>
struct TypeInfo<int> {
    static const char* name() { return "int"; }
---;

template <>
struct TypeInfo<std::string> {
    static const char* name() { return "std::string"; }
---;

// Usage
std::cout << TypeInfo<int>::name();           // "int"
std::cout << TypeInfo<double>::name();        // "unknown"

Partial Specialization

Specialize for a subset of types:

// Primary template
template <typename T, bool IsPod>
struct Serializer {
    static void write(const T& value) {
        std::cout << "Generic serialization\n";
    }
---;

// Partial specialization for POD types
template <typename T>
struct Serializer<T, true> {
    static void write(const T& value) {
        std::cout << "Binary serialization\n";
        // memcpy-style serialization
    }
---;

// Partial specialization for containers
template <typename T>
class MyVector<T*> {
    // Efficient implementation for pointer types
---;

// Partial specialization for function pointers
template <typename Ret, typename... Args>
struct FunctionTraits<Ret(*)(Args...)> {
    using return_type = Ret;
    using args_tuple = std::tuple<Args...>;
---;

Partial specialization works only for class templates (not function templates). It matches the most specialized version available.

Variadic Templates

Variadic templates accept any number of template arguments:

// Base case
void print() {}

// Recursive variadic function
template <typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << " ";
    print(rest...);
---

print(1, 2.5, "hello", 'c');  // Output: "1 2.5 hello c"

Fold Expressions (C++17)

Fold expressions simplify variadic template operations:

template <typename... Args>
auto sum(Args... args) {
    return (args + ...);       // Unary right fold
---

template <typename... Args>
auto sum_all(Args... args) {
    return (... + args);       // Unary left fold
---

template <typename... Args>
void print_all(Args... args) {
    ((std::cout << args << " "), ...);  // Fold over comma operator
---

// Binary fold
template <typename... Args>
bool all_true(Args... args) {
    return (true && ... && args);  // Initialize with true, then fold
---
Fold ExpressionExpansion
(... + args)((a + b) + c) + d
(args + ...)a + (b + (c + d))
(0 + ... + args)(((0 + a) + b) + c) + d
(args && ...)a && (b && (c && d))

Variadic Class Templates

template <typename... Types>
struct Tuple {};

// Recursive inheritance for heterogeneous containers
template <typename Head, typename... Tail>
struct Tuple<Head, Tail...> : Tuple<Tail...> {
    Head value;

    Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {}
---;

// Usage
Tuple<int, double, std::string> t(42, 3.14, "hello");

SFINAE (Substitution Failure Is Not An Error)

SFINAE is a principle that when template argument substitution produces invalid code, the compiler simply removes that overload from consideration rather than emitting an error.

enable_if

template <typename T>
typename std::enable_if<std::is_integral_v<T>>::type
process(T value) {
    std::cout << "Integral type: " << value << "\n";
---

template <typename T>
typename std::enable_if<std::is_floating_point_v<T>>::type
process(T value) {
    std::cout << "Floating point type: " << value << "\n";
---

int main() {
    process(42);      // Integral type
    process(3.14);    // Floating point type
    // process("hello");  // Error: no matching function
---

Modern SFINAE with decltype and void_t

// Check if a 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 {};

// Usage
static_assert(has_size<std::vector<int>>::value);
static_assert(!has_size<int>::value);

Expression SFINAE

template <typename T>
auto serialize(const T& value) -> decltype(value.serialize(), void()) {
    value.serialize();
---

template <typename T>
auto serialize(const T& value) -> decltype(to_string(value), void()) {
    std::string s = to_string(value);
---

template <typename T>
auto serialize(const T& value) -> std::enable_if_t<std::is_arithmetic_v<T>> {
    std::cout << value;
---

The comma operator trick lets you check multiple expressions: the last type before void() determines the return type, but all expressions must be valid.

C++20 Concepts

Concepts replace SFINAE with readable, reusable constraints:

#include <concepts>

template <typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
---;

template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

// Using concepts
template <Hashable T>
void insert_into_map(std::unordered_map<T, int>& map, T key, int value) {
    map[key] = value;
---

// Abbreviated syntax
void process(Numeric auto value) {
    std::cout << value * 2 << "\n";
---

// Compound requirements
template <typename T>
concept Comparable = requires(T a, T b) {
    { a == b } -> std::convertible_to<bool>;
    { a < b }  -> std::convertible_to<bool>;
---;

Nested Requirements and Type Constraints

template <typename T>
concept Container = requires(T c) {
    typename T::value_type;
    typename T::iterator;
    { c.begin() } -> std::same_as<typename T::iterator>;
    { c.end() }   -> std::same_as<typename T::iterator>;
    { c.size() }  -> std::convertible_to<std::size_t>;
---;

template <typename T>
concept SortableContainer = Container<T> && requires(T c) {
    std::sort(c.begin(), c.end());
---;

template <SortableContainer T>
void sort_and_print(T& container) {
    std::sort(container.begin(), container.end());
    for (const auto& v : container) {
        std::cout << v << " ";
    }
---

Concepts produce significantly better error messages than SFINAE. Instead of pages of substitution failures, the compiler says “constraint Hashable was not satisfied.”

Template Metaprogramming

Template metaprogramming performs computations at compile time:

Compile-Time Factorial

template <unsigned int N>
struct Factorial {
    static constexpr unsigned long value = N * Factorial<N - 1>::value;
---;

template <>
struct Factorial<0> {
    static constexpr unsigned long value = 1;
---;

static_assert(Factorial<5>::value == 120);

Compile-Time Prime Check

template <unsigned N, unsigned D>
struct IsPrimeHelper {
    static constexpr bool value =
        (N % D != 0) && IsPrimeHelper<N, D - 1>::value;
---;

template <unsigned N>
struct IsPrimeHelper<N, 1> {
    static constexpr bool value = true;
---;

template <unsigned N>
struct IsPrime {
    static constexpr bool value = IsPrimeHelper<N, N / 2>::value;
---;

template <>
struct IsPrime<0> : std::false_type {};
template <>
struct IsPrime<1> : std::false_type {};
template <>
struct IsPrime<2> : std::true_type {};

static_assert(IsPrime<17>::value);
static_assert(!IsPrime<15>::value);

Type Lists

template <typename... Types>
struct TypeList {};

// Get the first type
template <typename List>
struct Front;

template <typename Head, typename... Tail>
struct Front<TypeList<Head, Tail...>> {
    using type = Head;
---;

// Get the size of a type list
template <typename List>
struct Size;

template <typename... Types>
struct Size<TypeList<Types...>> {
    static constexpr std::size_t value = sizeof...(Types);
---;

using MyTypes = TypeList<int, double, std::string>;
static_assert(std::is_same_v<Front<MyTypes>::type, int>);
static_assert(Size<MyTypes>::value == 3);

constexpr If (C++17)

template <typename T>
auto get_value(const T& t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;
    } else {
        return t;
    }
---

if constexpr discards the non-taken branch at compile time, eliminating the need for SFINAE in many cases.

CRTP (Curiously Recurring Template Pattern)

CRTP enables static polymorphism:

template <typename Derived>
class Comparator {
public:
    bool operator<(const Derived& other) const {
        const auto& self = static_cast<const Derived&>(*this);
        return self.compare(other) < 0;
    }
---;

class Person : public Comparator<Person> {
    std::string name;
    int age;
public:
    int compare(const Person& other) const {
        if (age != other.age) return age - other.age;
        return name.compare(other.name);
    }
---;

CRTP avoids virtual function overhead while enabling mixin-like behavior.

Policy-Based Design

template <typename T, typename ThreadingModel>
class Container {
public:
    void insert(const T& value) {
        typename ThreadingModel::Lock lock;
        // insert logic
    }
---;

struct SingleThreaded {
    struct Lock {
        // No-op
    };
---;

struct MultiThreaded {
    struct Lock {
        std::mutex mtx;
        Lock() { mtx.lock(); }
        ~Lock() { mtx.unlock(); }
    };
---;

using SafeVector = Container<int, MultiThreaded>;
using UnsafeVector = Container<int, SingleThreaded>;

Best Practices

  • Prefer constexpr over template metaprogrammingconstexpr functions are easier to read and debug. Reserve template metaprogramming for type transformations.
  • Use concepts instead of SFINAE when using C++20 — concepts are more readable and produce better errors.
  • Keep template code minimal — Templates have longer compile times and produce harder-to-read errors. Factor out type-independent logic into non-template functions.
  • Use static_assert for better error messages — Check constraints early with clear messages.
  • Hide implementation details — Use detail namespaces for helper templates that users should not rely on.

Summary

Advanced templates unlock compile-time computation, type introspection, and flexible abstractions. Template specialization customizes behavior for specific types. Variadic templates handle arbitrary numbers of arguments. SFINAE enables compile-time overload resolution. C++20 concepts make constraints readable and produce clear error messages. Template metaprogramming computes values and manipulates types at compile time. CRTP and policy-based design provide static polymorphism and configurable behavior without runtime cost.

FAQ

What is template metaprogramming? Template metaprogramming uses C++ templates to perform computations at compile time rather than runtime. It enables type traits, static assertions, and policy-based design, shifting work from runtime to compilation.

What are variadic templates? Variadic templates accept any number of template arguments using typename... Args. Combined with parameter pack expansion, they enable type-safe variadic functions like printf and tuple implementations without macros.

What is SFINAE? Substitution Failure Is Not An Error — when template argument substitution produces invalid code, the compiler silently discards that overload instead of reporting an error. It enables compile-time introspection and conditional compilation.

What are C++20 concepts? Concepts constrain template parameters with compile-time predicates. They replace SFINAE with cleaner syntax: template <std::integral T> instead of std::enable_if. Concepts produce clearer error messages and more readable code.

How do I profile template-heavy code? Use compiler flags like -ftime-trace (Clang) and -ftemplate-depth- for controlling instantiation. Tools like Templight and C++ Insights help visualize template instantiation and debug compilation bottlenecks.

For a comprehensive overview, read our article on C File Io Guide.

For a comprehensive overview, read our article on C Memory Management.

Section: C/C++ 1611 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top