Skip to content

Instantly share code, notes, and snippets.

@matkatmusic
Created October 7, 2025 20:37
Show Gist options
  • Select an option

  • Save matkatmusic/ce460fba5f48baa788d819005ca9fd38 to your computer and use it in GitHub Desktop.

Select an option

Save matkatmusic/ce460fba5f48baa788d819005ca9fd38 to your computer and use it in GitHub Desktop.
A tutorial on how C++ Tag Dispatch works
/*
We want to design an interface for a function that allows us to pass different types to it.
Templates give us this functionality:
*/
template<typename T>
void function(const T& t)
{
doSomething(t);
}
/*
The way the above works, for every `T` that is passed to the function, there needs to be a corresponding `doSomething()` whose argument type matches.
What if we want these `doSomething` functions to work with multiple T types?
For example, lets assume we have 4 distinct types: A, B, C, D.
They don't inherit from a base class, either. They have zero relation with each other.
*/
struct A{};
struct B{};
struct C{};
struct D{};
/*
If we want a doSomething() implementation to work with both A and B, but not the others, how can we accomplish this?
if we write a templated doSomething, we get part way there:
*/
template<typename T>
void doSomething(const T& t) { }
/*
but we have to specialize for the other types:
*/
void doSomething(const C& t) { }
void doSomething(const D& t) { }
/*
And the templated version will be used for ANY type of T that is not C or D:
*/
struct FooBar {};
void AnyTypeOfT()
{
doSomething(FooBar{});
}
/*
We want the templated version to be *restricted* to only working with A and B.
How can we accomplish this restricting of who can call that function?
We can use the C++ Type system and Argument Dependent Lookup, and add a 2nd argument, whose purpose is to leverage ADL to pick the correct function to call *at compile time*.
ex:
template<typename T>
void doSomething(const T& t, Tag) { }
Now, we can have multiple Tag types, also declared as distinct structs:
*/
struct TagForAB {};
struct TagForC {};
struct TagForD {};
/*
And now we can write our generic doSomething function using those tags instead:
*/
template<typename T>
void doSomething(const T& t, TagForAB tfab) { }
template<typename T>
void doSomething(const T& t, TagForC tfc) { }
template<typename T>
void doSomething(const T& t, TagForD tfd) { }
/*
Now we have 3 versions of the doSomething function that can be called for Any T, and the Tag is what decides which version gets called.
In order to pick the correct doSomething in the process function, we need to choose the right Tag.
For that, we use Tag Dispatch and the C++ feature of Nested Types.
Consider the following class:
*/
struct Dispatch1
{
using type = TagForAB;
};
/*
Now consider this class:
*/
struct Dispatch2
{
using type = TagForAB;
};
/*
The only difference is the class name.
We can leverage Template Partial Specialization to give these classes the same name, and thus the same 'using type =' pattern.
Template Partial Specialization allows customizing class and variable(since C++14) templates for a given category of template arguments.
First, we forward declare a generic templated class.
We forward declare instead of define because we only want our distinct definitions to exist. we don't want the compiler to generate the definitions for us.
*/
template<typename T>
struct DispatchTag;
/*
Then we specialize it for our struct A, and give it a nested type, in the form of a type alias.
*/
template<>
struct DispatchTag<A> { using type = TagForAB; };
/*
The member 'type' is an alias for the specific struct we want to use when ADL is invoked when calling the doSomething() function.
We'll do the same for B, with an identical type alias.
*/
template<>
struct DispatchTag<B> { using type = TagForAB; };
/*
Notice that both of these specializations have the same nested type, and the nested type's name is also the same (the 'using type =' part of the expression. ).
Because this nested typename's class name is the same as, we can write a templated helper alias to access that nested typename alias, and pass it any T that we've defined a partial template specialization for:
*/
template<typename T>
using DispatchTagType = DispatchTag<T>::type;
/*
When we write DispatchTagType<A>, we are writing an alias for TagForAB:
*/
#include <type_traits>
void AssertDispatchTagTypeForA()
{
static_assert( std::is_same_v<DispatchTagType<A>, TagForAB>, "matches");
//static_assert( std::is_same_v<DispatchTagType<A>, TagForC>, "matches"); //fails
}
/*
let's add a specialization for C:
*/
template<>
struct DispatchTag<C> { using type = TagForC; };
/*
Let's confirm its tag type is TagForC:
*/
void AssertDispatchTagTypeForC()
{
static_assert( std::is_same_v<DispatchTagType<C>, TagForC>, "matches");
//static_assert( std::is_same_v<DispatchTagType<C>, TagForAB>, "matches"); //fails
}
/*
Now that we have this DispatchType tool, we can use it to select the 'Tag' for us *at compile Time*.
Just remember that the {} after DispatchTagType<T> is declaring an instance of the alias, which, depending on what T is, could be TagForAB, TagForC, or TagForD, just like we want.
*/
template<typename T>
void process(const T& t)
{
doSomething( t, DispatchTagType<T>{} );
}
/*
We can add safety to using the `process()` function by using a Concept to Require that DispatchTag<T> is defined and has the nested typename 'type':
*/
#include <concepts>
template<typename T>
concept HasDispatchTag = requires
{
typename DispatchTag<T>::type;
};
/*
this concept is saying that there must be a templated class named DispatchTag, and when invoked with T as the template argument, a nested Typename called 'type' must exist.
Remember: the generic template for DispatchTag<> was a *Forward Declaration*. it was not a real declaration. So only our partial specializations are actually defined.
*/
template<typename T>
requires ( HasDispatchTag<T> )
void process(const T& t)
{
doSomething(t, DispatchTagType<T>{});
}
/*
now we can invoke our process function with distinct types, and the Tag selection performed by DispatchTagType<T>{} will tell the compiler which version of doSomething() to call:
doSomething(t, TagForAB);
doSomething(t, TagForC);
doSomething(t, TagForD);
*/
void processABC()
{
process( A{} );
process( B{} );
process( C{} );
}
/*
If we call process( D{} );, we will get an error because we never defined a partial template specialization of DispatchTag for D.
*/
void processD()
{
//process( D{} ); //error: implicit instantiation of undefined template 'DispatchTag<D>'
}
/*
however, if we define a DispatchTag for D, the error will go away:
*/
template<>
struct DispatchTag<D> { using type = TagForD; };
void processDCorrectly()
{
process( D{} );
}
/*
Tag Dispatch is a powerful combination of Argument Dependent Lookup, the type system, templated classes, and template partial specialization that allows the compiler to decide which function to call, instead of the C++ runtime, as would be the case if inheritance was used. This means the code ultimately runs faster, though it is more complex. But we use C++ for speed, not for simplicity.
*/
int main()
{
processABC();
processD();
processDCorrectly();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment