Skip to content
C++
Idiom
since C++98
Intermediate

Tag Dispatch

Select function overloads at compile time by passing empty tag structs. The canonical pre-C++17 technique for type-based branching without SFINAE.

Tag Dispatchsince C++98

Tag dispatch selects a function overload at compile time by passing a default-constructed empty struct whose type encodes the desired code path, delegating the selection to the compiler's overload resolution rather than to runtime conditionals or SFINAE.

Overview

Before C++17's if constexpr and C++20's concepts, tag dispatch was the primary mechanism for branching on type properties inside function templates. The technique works by introducing a tag type β€” an empty struct whose identity carries semantic meaning β€” and passing a default-constructed instance of it to an overloaded implementation function.

The compiler picks the correct overload at zero runtime cost. Because overload resolution operates on types, not values, the tag object is never stored or examined; it exists only to direct the compiler.

The standard library has used this pattern since C++98 in <algorithm> and <iterator>. std::advance, std::distance, and std::next all dispatch on iterator category tags to select O(1) versus O(n) implementations.

Tag dispatch remains relevant even in C++20 codebases for two reasons that neither if constexpr nor concepts replicate well: priority chains (controlled fallback ordering via tag inheritance) and open-world extensibility (users add overloads in their own namespace; the dispatch function never needs to know about them).

Syntax

cpp
// Step 1: define tag types
struct forward_path {};
struct random_access_path {};

// Step 2: write an overloaded _impl for each tag
template<typename Iter>
void advance_impl(Iter& it, int n, forward_path) {
    while (n-- > 0) ++it;
}

template<typename Iter>
void advance_impl(Iter& it, int n, random_access_path) {
    it += n;  // O(1)
}

// Step 3: public dispatch function constructs the tag
template<typename Iter>
void my_advance(Iter& it, int n) {
    using Cat = typename std::iterator_traits<Iter>::iterator_category; // C++98
    using Tag = std::conditional_t<                                     // C++14
        std::is_base_of_v<std::random_access_iterator_tag, Cat>,        // C++17
        random_access_path,
        forward_path>;
    advance_impl(it, n, Tag{});
}

The dispatch function is the only public surface. Callers never see the tags; the _impl overloads are implementation detail.

Examples

Iterator Category Dispatch (the canonical example)

The standard iterator tag types form an inheritance hierarchy (since C++98), and the compiler's overload resolution picks the most-derived match:

cpp
#include <iterator>

// std tag hierarchy:
//   input_iterator_tag
//   └─ forward_iterator_tag
//      └─ bidirectional_iterator_tag
//         └─ random_access_iterator_tag  (C++98)
//            └─ contiguous_iterator_tag  (C++20)

template<typename Iter, typename Distance>
void advance_impl(Iter& it, Distance n, std::input_iterator_tag) {
    // Covers forward iterators too via implicit upcast
    while (n-- > 0) ++it;
}

template<typename Iter, typename Distance>
void advance_impl(Iter& it, Distance n, std::bidirectional_iterator_tag) {
    if (n >= 0) while (n-- > 0) ++it;
    else        while (n++ < 0) --it;
}

template<typename Iter, typename Distance>
void advance_impl(Iter& it, Distance n, std::random_access_iterator_tag) {
    it += n;  // O(1) β€” also covers contiguous_iterator_tag (C++20)
}

template<typename Iter, typename Distance>
void my_advance(Iter& it, Distance n) {
    // Construct the category tag; overload resolution picks the best match
    advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

A std::list iterator carries bidirectional_iterator_tag. A std::vector iterator carries random_access_iterator_tag. The right overload is chosen without any runtime check.

true_type / false_type β€” the simplest tags

std::true_type and std::false_type (C++11) are themselves tag types. Any type trait that derives from them can be used directly as a tag:

cpp
#include <type_traits>
#include <cstring>

template<typename T>
void copy_n_impl(const T* src, T* dst, std::size_t n, std::true_type) {
    std::memcpy(dst, src, n * sizeof(T));   // trivially copyable: raw copy
}

template<typename T>
void copy_n_impl(const T* src, T* dst, std::size_t n, std::false_type) {
    for (std::size_t i = 0; i < n; ++i)
        new (dst + i) T(src[i]);            // requires copy constructor
}

template<typename T>
void copy_n(const T* src, T* dst, std::size_t n) {
    copy_n_impl(src, dst, n,
        std::is_trivially_copyable<T>{});   // C++11
}

Priority Chains via Tag Inheritance

Tag inheritance lets you express "prefer A over B over C" without SFINAE gymnastics. Pass the highest-priority tag; the compiler walks up the hierarchy until it finds a viable overload:

cpp
struct priority_low    {};
struct priority_mid    : priority_low  {};
struct priority_high   : priority_mid  {};

// Called only if T has a .serialize() member
template<typename T>
auto to_string_impl(const T& v, priority_high)
    -> decltype(v.serialize())
{
    return v.serialize();
}

// Called if T works with std::to_string (C++11)
template<typename T>
auto to_string_impl(const T& v, priority_mid)
    -> decltype(std::to_string(v))
{
    return std::to_string(v);
}

// Unconditional fallback
template<typename T>
std::string to_string_impl(const T&, priority_low) {
    return "[opaque]";
}

template<typename T>
std::string to_string(const T& v) {
    return to_string_impl(v, priority_high{});  // start at top of chain
}

When T has .serialize(), the high overload wins. If not, the SFINAE on the trailing return type eliminates it, and mid is tried. The unconditional low overload is always available as a backstop. This pattern is significantly cleaner than nested std::enable_if chains.

Best Practices

Keep tags in a details namespace. Exposing advance_impl and the tag types in the public API clutters the interface. Put them in a detail:: namespace or an anonymous namespace.

Prefer std::true_type/std::false_type for binary dispatch. Rolling custom tag types for a two-way branch adds ceremony without benefit. Use std::is_trivially_copyable<T>{} directly.

Use priority chains instead of SFINAE stacking. When you have more than two candidates, priority-tag inheritance is more readable and easier to extend than a chain of std::enable_if conditions.

Design tags for open-world extensibility. If users of your library should be able to add dispatch paths for their types, expose the tag types and the _impl name in your namespace. ADL will find their overloads without requiring changes to your dispatch function.

Match difference_type precisely in iterator dispatch. Standard iterator algorithms use iterator_traits<Iter>::difference_type, not int. Use the same type in your _impl signatures to avoid implicit conversion warnings.

Common Pitfalls

Overload resolution walks the inheritance chain upward. A random_access_iterator_tag implicitly converts to bidirectional_iterator_tag and all its ancestors. If you omit the bidirectional overload, random-access iterators fall through to the input overload β€” silently, without a compile error. Audit every tier of the chain.

ADL can silently select the wrong overload. If your tag type and implementation functions live in different namespaces, ADL may not find the right overload. Keep tags and their _impl functions in the same namespace.

if constexpr is not always a drop-in replacement. if constexpr branches are syntactically inside a single function: both branches must parse, and only instantiation is suppressed. Tag dispatch sends each path to a separate function, so an overload whose body is entirely invalid for a given type simply won't be instantiated at all. Priority-chain dispatch also has no direct if constexpr equivalent.

Priority chains require the fallback to be unconstrained. The low_priority overload must accept any T. If you add a requires clause or a SFINAE condition to it, types that fall through every higher-priority overload will produce a hard error rather than a graceful fallback.

See Also

  • std::iterator_traits β€” the traits class whose iterator_category typedef is the tag
  • SFINAE β€” the alternative mechanism for overload removal; heavier syntax, same compile-time effect
  • if constexpr β€” C++17 single-function alternative for simpler branch scenarios
  • Concepts β€” C++20 overload constraints that supersede tag dispatch for most new code
  • CRTP β€” another compile-time polymorphism technique, complementary to tag dispatch