C++ Template Specialization and Class Template
This is an episode of this series
Template Specialization
Imagine you want to implement an add function for float, double, and string. For numeric types, we can implement a generic template function using
template <typename T>
T add(const T& a, const T& b){
return a + b;
}
However, for a string, e want to concatenate them. So, we want to specify a template function for string specifically. That’s where template specialization comes in:
#include <sstream>
#include <string>
template <>
std::string add(const std::string& a, const std::string& b){
std::stringstream st;
st << a << b;
return st.str();
}
int main()
{
std::cout<<add(std::string("a"),std::string("b"));
return 0;
}
Note:
- We are using the
stringstream
object to concatenate strings. It’s in<sstream>
instead of<iostream>
because there’s no file/console I/O operations involved - One might expect
add("a", "b")
through implicit conversion would work, but actually it doesn’t . Cpp disables implicit coversion when deducing template parameters. If you are curious about other scenarios where implicit conversion is disabled, some common ones are: constructor with the explicit keyword, overloading function resolution (exact matches are preferred over conversions), and list initialization (where narrowing conversion is banned)
What About Non-Template Overloading Function?
When provided with a non-template overloading function, C++ prefers that:
std::string add(const std::string& a, const std::string& b){
return "lol";
}
However, template specialization is still needed:
- When working with STL functions: they are not overloadable, but can be specified template specialized.
Template Struct And Its Specialization
If we want to expand add()
into a “stateful” accumulator class, say if we want to keep track of how variables of the same type get accumulated onto each other. Then, we can use a class/struct templates
// T is & must be figured out during compile time.
// So if T is determined during run time, we will see an "non-type" template argument is not a constant
template<typename T=int>
struct Accumulator{
T val_;
Accumulator(const T& init){
val_ = init;
};
void add(const T& a){
val_ += a;
}
};
Note:
- One can declare a default type in
T
so the typename is optional.
Similarly, with string
, we can have a specialized class template
template<>
struct Accumulator<std::string>{
std::string val_;
Accumulator(const std::string& init){
val_ = init;
}
// this could be optimized!
void add(const std::string& a){
std::stringstream ss;
ss << val_ << a;
val_ = ss.str();
}
};
Note:
- We need to specify
<std::string>
for a specialized class template to clearly indicate its type
Real World Usage Example
Let’s create a hash function for a custom struct Pixel2D
struct Pixel2D{
int x, y
};
Because std::hash
is defined as a struct template, we can’t overload it as a function. We can only define a specialized template
namespace std {
template <>
struct hash<Rigid2D::Pixel2D>{
std::hash<long> _hasher;
size_t operator() (const Rigid2D::Pixel2D& p) const {
// shifting 4 bytes
constexpr size_t _shift_bits_num = sizeof(int) * 8;
return _hasher( static_cast<long>(p.x) << _shift_bits_num | static_cast<long>(p.y));
}
};
}