Skip to content

Template Specialization & Tag Dispatch

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

Why Specialize?

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 specialization that packs bools into bits for space efficiency (though it is widely considered a mistake in the standard library).

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

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

Full Specialization

A full specialization 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 Specialization

Partial specialization 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 specialization.

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 specialized — only fully specialized. Worse, function template specializations 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 specialization.

This leads to surprising behavior:

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

Calling f((int*)nullptr) selects #2 (the better overload match), not #3 (which specializes #1, not #2). The fix: use regular function overloading instead of function template specialization. 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.

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.
- Full specialization — use for class templates when you need a completely different implementation for one specific type.
- Never use function template specialization — use overloading instead.

Key Takeaways
  • Full specialization (template <>) provides a custom implementation for one specific type
  • Partial specialization constrains some parameters — available for class templates only, not function templates
  • Function template specialization 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 specializing function templates?

2. Which types of templates support partial specialization?

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