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!