Skip to content

Template Specialisation & Tag Dispatch

Master full and partial specialisation, learn when to prefer overloading, and discover tag dispatch and if constexpr as alternatives for compile-time branching.

Why Specialise?

The primary template provides a generic implementation that works for most types. But sometimes a specific type needs fundamentally different treatment. For example, std::vector is a specialisation that packs bools into bits for space efficiency (though it is widely considered a mistake in the standard library, and is rather like that one relative nobody mentions at Christmas).

Template specialisation lets you provide an alternative definition for specific template arguments. There are two kinds:

1. Full (explicit) specialisation — you fix all template parameters to specific types.
2. Partial specialisation — you fix some parameters or constrain them (e.g., specialise for all pointer types). Partial specialisation is only available for class templates, not function templates.

Full Specialisation

A full specialisation provides a completely custom implementation for one specific set of template arguments. The syntax uses template <> (an empty parameter list) followed by the class or function with concrete arguments.

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

Partial Specialisation

Partial specialisation constrains some template parameters while leaving others generic. This is extremely powerful for matching families of types — all pointers, all arrays, all pairs, etc. Only class/struct/variable templates support partial specialisation.

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
Pitfall

Function templates cannot be partially specialised — only fully specialised. Worse, function template specialisations do not participate in overload resolution the way you might expect. The compiler first selects the best base template via overload resolution, and only then checks if that base template has a matching specialisation.

This leads to surprising behaviour:

template <typename T> void f(T)   { /* #1 - primary */ }
template <typename T> void f(T*)  { /* #2 - overload for pointers */ }
template <> void f<int*>(int*)     { /* #3 - specialisation of #1 */ }

Calling f((int*)nullptr) selects #2 (the better overload match), not #3 (which specialises #1, not #2). It is the C++ equivalent of queueing patiently and still being served last. The fix: use regular function overloading instead of function template specialisation. If you need compile-time type branching, use if constexpr or concepts.

Tag Dispatch Pattern

Tag dispatch uses empty struct types ("tags") to select overloads at compile time. The standard library uses this extensively — std::advance dispatches on iterator category tags. This was the dominant compile-time branching technique before if constexpr.

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

if constexpr: The Modern Alternative

C++17's if constexpr provides a cleaner way to branch at compile time within a single function. The discarded branch is not instantiated, so it does not need to compile for the given type. This largely replaces tag dispatch and SFINAE for simple cases, and spares you a good deal of fuss.

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
Best Practice

Use this decision guide for compile-time branching:

- if constexpr (C++17) — simplest, works within a single function, no special patterns needed. Prefer this for straightforward type-based logic.
- Concepts/requires (C++20) — best for constraining function overloads or entire templates. Self-documenting and gives clear error messages.
- Tag dispatch — still useful when you have an existing tag hierarchy (like iterator categories). Pre-C++17 codebases rely heavily on this.
- SFINAE / enable_if — the oldest technique. Harder to read, cryptic errors. Avoid in new code if concepts or if constexpr work. Reading its error messages is a bit like deciphering a stranger's handwriting.
- Full specialisation — use for class templates when you need a completely different implementation for one specific type.
- Never use function template specialisation — use overloading instead.

Key Takeaways
  • Full specialisation (template <>) provides a custom implementation for one specific type
  • Partial specialisation constrains some parameters — available for class templates only, not function templates
  • Function template specialisation is error-prone — prefer overloading or if constexpr
  • Tag dispatch uses empty struct types to select overloads at compile time via iterator_traits
  • if constexpr (C++17) is the modern, cleaner alternative that discards untaken branches at compile time

Quiz — Test Your Knowledge

(20 XP)

1. Why should you avoid specialising function templates?

2. Which types of templates support partial specialisation?

3. What does `if constexpr` do differently from a regular `if`?