[C++] Tag Dispatching To Overload Functions
In my last article I wrote about function overloading by returntype and while I'm reading C++ Templates: The Complete Guide I came across tag dispatching. After reading different blog posts about it, I want to provide some examples here.
First of all, let's consider the following problem. We have a function do_some_work
and we want to overload it with multiple templates. Then this definition would lead to a multiple definition error:
template<typename T>
void do_some_work(T&& t)
{
}
template<typename S>
void do_some_work(S&& s) // ERROR: cannot be overloaded with...
{
}
Same applies to this class:
class worker
{
public:
template<typename T>
void do_some_work(T&& t)
{
}
template<typename S>
void do_some_work(S&& s) // ERROR: cannot be overloaded with...
{
}
};
Of course, we could just use different function names. But sometimes you simply want to overload it.
Define Tags For Different Types
You can find this example here on compiler explorer.
We'll define type dependend tags. This means depending on the type which is passed to our function, we'll decide which implementation to call.
- Define empty structs, which represents the tag ->
// #1
- Define a struct which holds each tag ->
// #2
- Implement the functions which are supported ->
// #3
namespace detail {
// #1
struct int_tag {};
struct string_tag {};
// #2
template <typename T>
struct traits {};
template <>
struct traits<int> {
using tag = int_tag;
};
template <>
struct traits<std::string_view> {
using tag = string_tag;
};
// #3
template <typename T>
void do_some_work(const T& val, const int_tag& tag) {
std::cout << "integer implementation with: val = " << val << std::endl;
}
template <typename T>
void do_some_work(const T& val, const string_tag& tag) {
std::cout << "string implementation with: val = " << val << std::endl;
}
} // namespace detail
template <typename T>
void do_some_work(const T& val) {
detail::do_some_work(val, typename detail::traits<T>::tag());
}
Note: do_some_work
inside the detail namespace has another function argument which represents the tag. This means in reality each do_some_work
function is overloaded with a specific tag and which is the reason this works in the first place.
And now you can add different implementations depending on the type.
You can find this example here on compiler explorer.
Using std::true_type
and std::false_type
You can find this example here on compiler explorer.
Let's consider another example, where you have a class which can hold a value. To access it we have to methods. One method which returns the value and one alternative which possibly returns the value (similar to std::optional). Take a look at the following templated class where the type depends on it's instantiation:
template<typename T>
class worker
{
public:
T get_value() {
return *m_value.get();
}
template<typename U>
auto get_value_or(U&& u);
private:
std::unique_ptr<T> m_value;
};
And we'd have the following behaviour of get_value_or(...)
:
- If there is a value in
m_value
then return it. - If there is no value in
m_value
then we return the value which was given in the function argument - If the function argument is something callable, then we call it (like a lambda)
One possible implementation would something like this:
template<typename T>
class worker
{
public:
T get_value() {
return *m_value.get();
}
template<typename U>
auto get_value_or(U&& u) {
if (m_value) {
return get_value();
} else {
return get_value_or(u, std::is_invocable<U>{});
}
}
private:
std::unique_ptr<T> m_value;
template <typename Func>
auto get_value_or(Func&& func, std::true_type) {
return func();
}
template <typename V>
auto get_value_or(V&& v, std::false_type) {
return v;
}
};
We introduce two private functions get_value_or
with an additional parameter. This allows us to overload the public get_value_or
member function. This is either takes an additional std::true_type
or std::false_type
as second argument.
We create std::true_type
/ std::false_type
by using std::is_invocable<>{}
.
If you haven't heard about std::true_type
/ std::false_type
you can start with this stackoverflow question.
We could use worker
then like this:
// worker with type int underneath
worker<int> w;
// prints "let's do something here... " and returns 0
w.get_value_or([](){
std::cout << "let's do something here... ";
return 0;
});
// result will be 5
int result = w.get_value_or(5);
Note: We are a bit limited here because we need to return the same type as we store in m_value
. This applies also for lambdas or function pointers we could pass to get_value_or
.
You can find this example here on compiler explorer.
Conclusion
I pretty much like this approach to overload functions. It's also usefull to hide some implementation details and can make code more idiomatic to write. And you get rid of this sort of annoying code:
some_possible_pointer p = worker.get();
if (p) {
// do some work here
}
Maybe you already know Tag Dispatching and maybe you learnt something here. I hope this could help.
But that's it for now.
Best Thomas