Contents
There are some nice-to-know use cases for classes that have no members. One of them is tag dispatch. It can even be found in your standard library implementation.
When I write empty classes, I really mean empty. Classes without data can be found everywhere. Abstract base classes of class hierarchies often carry only declarations of virtual functions. Classes used in template metaprogramming may contain only typedefs or static members that are used at compile time.
That’s not what I write about today. I mean really empty, no data, no functions, no typedefs.
class Empty {};
Tag dispatch
Empty classes can be useful in C++ because it is a strongly typed language. If there are two empty classes, they are different types. They don’t have anything to do with each other. Objects of those types can not be converted to each other. There is nothing we can do with them but construct them and let them get destroyed.
Except we can pass them to functions. That is where tag dispatch comes in. It usually is used in generic programming, i.e. in templates. Imagine two or more alternatives of a function that take the same list of parameters. They need to have the same name but different implementations. How do we distinguish between the two?
A function signature is defined by its name and the types of its parameters. That information is what the compiler uses to look up the right function to call. So, to have more than one function identical in this regard, we have to add something that helps the compiler telling them apart. This can be achieved by adding a sentinel parameter type that is not used.
struct Variant1Tag {};
struct Variant2Tag {};
void functionWithVariants(int i, double d, std::string str, Variant1Tag) {
// ...
}
void functionWithVariants(int i, double d, std::string str, Variant2Tag) {
// ...
}
You see here that the tag type makes the two functions have different parameter lists. That way we can tell the compiler which one to use.
functionWithVariants(42, 3.14, "less obvious values next time", Variant1Tag{});</pre>
Use case example
A prominent case where this technique usually is used is the constructor of std::vector
. vector
has many constructors. One of them takes a pair of iterators to a range of values that shall be copied into the newly constructed vector
. Another takes a size_type N
and a value. It constructs the vector with N
copies of that value.
So far that doesn’t sound too bad. We can write the signatures pretty fast:
template <class T>
class vector {
public:
vector(size_type N, T const& value = T());
template <class It>
vector(It first, It last);
};
(I left out the optional allocator parameters for simplicity)
Now imagine a vector<int>
. We want to construct it containing four elements with the value 32:
vector<int> v(4, 32);
size_type
is an unsigned integral type. Therefore to call the constructor we want to be called, the compiler would have to convert the int 4
into a size_type
. But there is an overload of the constructor taking two arguments of the same type! The constructor we meant to be used for iterators is the better match! We can not do anything against that, except explicitly casting the 4 to vector<int>::size_type
, which is quite ugly to type and read.
For that reason, until C++11, the templated constructor had the same effect as the other constructor, if It
turned out to be not really an input iterator type. (Today the iterator version does not take part in overload resolution if It
is not an iterator)
Tag dispatch can be used to distinguish between the iterator version and the integral type version of the constructor, using the iterator tags of the standard library.
template <class It>
vector<T>::vector(It first, It last) {
typedef get_iterator_tag_for<It>::type tag_type;
construct(first, last, tag_type{});
}
template <class It>;
vector<T>::construct(It first, It last, std::input_iterator_tag) {
// construct iterator style
}
template <class Int>
vector<T>::construct(Int N, Int const& value, SomeOtherTag) {
// construct with N copies of value
}
What if we want to store a bit more information in our tags than just a type name to distinguish things? We can do that with templates. Tags are used to distinguish functions at compile time, so the compile-time information encoded in templates can come in handy.
The above example of the iterators basically contained a boolean information: Is the parameter type an iterator or not? So, instead of having different named types we could also have used a template. Be careful to not use std::true_type
and std::false_type
in a boolean situation like that, because only seeing the call contruct(first, last, std::true_type{})
would not be very informative (what is true?).
Instead, a well-named tag template will make very clear what is going on:
template <bool>
struct UseIteratorSemantics
{};
//...
construct(first, last, UseIteratorSemantics<true>{});
A note on performance
Although performance should not be our first concern, it clearly matters if we are talking about general utilities like std::vector
that can be used everywhere. So what is the performance and memory overhead of constructing and passing around tags like this?
The answer is zero. Nothing. Since we are usually talking about templates here, the compiler can see the function definition and that the tag is never used. Therefore, it can optimize away the extra parameter, its construction and everything related easily.
Permalink
Permalink
what does this even mean?
vector(size_type N, T const& value = T());
I understand that it is the constructor, but it is taking an arbirary type, then saying that is a const, then creating a value, then setting that value to be equal to the constructor of the arbtraty object.
I don’t get this at all. Really appreaciate the help.
Permalink
Hi Lee,
yes, it is a constructor. The first argument is
size_type
, which is a typedef inside thevector
class template and usually is an alias forstd::size_t
, which usually is something likeunsigned long long
.The second argument is of type
T const&
. It is not an arbitrary type, it is the template parameter of the vector class template. Meaning, for avector<int>
, that second argument isint const&
, for avector<std::string>
it isstd::string const&
and so on.The
= T()
is simply a default for that second parameter, meaning if we call the constructor with only one parmeter, it is called with a default constructedT
.So, writing
std::vector<int> myNumbers(42);
will be the same asstd::vector<int> myNumbers(42, int());
orstd::vector<int> myNumbers(42, 0);
i.e. it is initialized with 42 zeroes.Side note:
int const&
is the exact same asconst int&
– I’m using “East Const” notation here.Permalink
Fantastic article that helped to UNDERSTAND iterator tags.
In order to complete myself though, I needed to see an implementation of get_iterator_tag_for, which is used in typedef get_iterator_tag_for::type tag_type;
Thanks!
Permalink
Permalink
Nice post!.
gnu’s _pb gave me creeps when i opened source code. but makes sense now.
thanks!
Permalink
Permalink
I think it’s worth mentioning a special case of this, and that is to dispatch calls with valid template parameters to a function that can do its job, and to fail with a clean compilation error message otherwise.
Example. Say you have a function that only works on integral types, but works on all integral types:
with func_impl() as below:
Non-integral types are dispatched to the failing version of func_impl that is not implemented. That means that the all too classic avalanche of template error messages is cut short. For this technique, using std::true_type and std::false_type is OK, because you use the static_assert() to give a meaningful message.
Permalink
I fixed the code above for future readers:
template
T func_impl(T, std::false_type); // not implemented, does nothing.
template
T func_impl(T t, std::true_type) // does the real job
{
return t;
}
template
T func(T t)
{
auto constexpr integral_param = std::is_integral::value;
static_assert(integral_param, “func(T) requires T to be an integral type”);
return func_impl(t, std::integral_constant<bool, integral_param>{});
}
Permalink
it seems like the “” in “std::is_integral<T>::value;” did not go through in my previous comment…