BOOST Code Reading - Tag Dispatching

Preface

In real programming world, it’s not unusual that the function we implemented is designed for multiple types. For example, for a function calc(x, exp) that receive x and exp as parameter and return x * 2^{exp}. It should be working for both integer type and decimal type. Obviously, we can implement it as follows:

template<typename num_t>
num_t calc(num_t x, int exp) {
    return exp >= 0 ? x * (1 << exp) : x / (1 << exp);
}

But wait for a minute, this kind of implementation is not efficient for integer type. For type of integer, we can write it as follows:

template<typename int>
int calc(int x, int exp) {
    return x << exp;
}

To combine both of the implementation, we can write it as follows:

template<typename num_t>
num_t calc(num_t x, int exp) {
    return exp >= 0 ? x * (1 << exp) : x / (1 << exp);
}

template<typename int>
int calc(int x, int exp) {
    return x << exp;
}

It seems that we have successfully solved the problem! But think about it, we have long long long short int all of them belong to integer type and double float long double all of them belong to decimal type. Does it means that we need to copy the code above over and over again and modify the type? And thinking further, if we have $m$ types and $n$ functions. It means that we need to write $n\times m$ times in total. The answer is absolutly NOT, here we have a more efficient solution.

Traits Class

Firstly, we need to introduce the tratis class. The idea of traits class is simple. For different types, there may be some different information that we need. Let’s see a simplest one:

template<typename num_t>
struct numerical_traits {
    typedef num_t type;
};

And we can deduce type from it.

template<typename num_t>
num_t calc(num_t x, int exp) {
  typename numerical_traits<num_t>::type tmp = exp;
    return exp >= 0 ? x * (1 << exp) : x / (1 << exp);
}

This approach may seems trivial, but when this technique is combined with tags and template specialization, it will be very useful.

Tag Dispatching

When we talked about a tag, it means a empty struct

struct integer_tag {};
struct decimal_tag {};

We can use it to determine different kind of type. All we need to do is specify which type own which tag. At this time, traits class is employed

template<typename num_t> 
struct numerical_traits {};

template<>
struct numerical_traits<int> {
    typedef integer_tag tag;
};

template<>
struct numerical_traits<double> {
    typedef decimal_tag tag;
};

Further more, we can specify float own numerical_tag and long long own integer_tag once for all.

Now we can use tags to distinguish different implementation

template<typename num_t> 
num_t calc(num_t x, int exp, integer_tag) {
    std::cout << "integer version called\n";
    return x << exp;
}

template<typename num_t> 
num_t calc(num_t x, int exp, decimal_tag) {
    std::cout << "decimal version called\n";
    return exp >= 0 ? x * (1 << exp) : x / (1 << exp);
}

Finally, we just need to wraper all of them into one function

template<typename num_t>
num_t calc(num_t x, int exp) {
    typedef typename numerical_traits<num_t>::tag tag;
    return calc(x, exp, tag());
}

And we can use a sample to test it

int main() {
    calc(static_cast<int>(10), 10);
    calc(static_cast<double>(10), 10);
    return 0;
}

Output

integer version called
decimal version called

By using this technique, for $n$ types and $m$ functions, we just need to write $n + m$ pieces of code. Awesome!