C++ Template Specialization and Class Template

Rico Ruotong Jia
2 min readFeb 10, 2024

--

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));
}
};
}

--

--

Rico Ruotong Jia
Rico Ruotong Jia

Written by Rico Ruotong Jia

A singer, a gym goer, a robot enthusiast, a human being. Northwestern University MSc. Robotics

Responses (1)