19.1 — Template classes
Templates and container classes
Template classes in the standard library
Now that we’ve covered template classes, you should understand what std::vector means now – std::vector is actually a template class, and int is the type parameter to the template! The standard library is full of predefined template classes available for your use. We’ll cover these in later chapters.
Splitting up template classes
19.2 — Template non-type parameters
Non-type parameters
#include <iostream>
template <typename T, int size> // size is an integral non-type parameter
class StaticArray
{
private:
// The non-type parameter controls the size of the array
T m_array[size] {};
public:
T* getArray();
T& operator[](int index)
{
return m_array[index];
}
};
// Showing how a function for a class with a non-type parameter is defined outside of the class
template <typename T, int size>
T* StaticArray<T, size>::getArray()
{
return m_array;
}
int main()
{
// declare an integer array with room for 12 integers
StaticArray<int, 12> intArray;
// Fill it up in order, then print it backwards
for (int count { 0 }; count < 12; ++count)
intArray[count] = count;
for (int count { 11 }; count >= 0; --count)
std::cout << intArray[count] << ' ';
std::cout << '\n';
// declare a double buffer with room for 4 doubles
StaticArray<double, 4> doubleArray;
for (int count { 0 }; count < 4; ++count)
doubleArray[count] = 4.4 + 0.1 * count;
for (int count { 0 }; count < 4; ++count)
std::cout << doubleArray[count] << ' ';
return 0;
}
19.3 — Function template specialization
When instantiating a function template for a given type, the compiler stencils out a copy of the templated function and replaces the template type parameters with the actual types used in the variable declaration. This means a particular function will have the same implementation details for each instanced type (just using different types). While most of the time, this is exactly what you want, occasionally there are cases where it is useful to implement a templated function slightly different for a specific data type.
Template specialization is one way to accomplish this.
Let’s take a look at a very simple template class:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
The above code will work fine for many data types:
int main()
{
// Define some storage units
Storage<int> nValue { 5 };
Storage<double> dValue { 6.7 };
// Print out some values
nValue.print();
dValue.print();
}
This prints:
5
6.7
Now, let’s say we want double values (and only double values) to output in scientific notation. To do so, we can use a function template specialization (sometimes called a full or explicit function template specialization) to create a specialized version of the print() function for type double. This is extremely simple: simply define the specialized function (if the function is a member function, do so outside of the class definition), replacing the template type with the specific type you wish to redefine the function for. Here is our specialized print() function for doubles:
template <>
void Storage<double>::print()
{
std::cout << std::scientific << m_value << '\n';
}
When the compiler goes to instantiate Storage::print(), it will see we’ve already explicitly defined that function, and it will use the one we’ve defined instead of stenciling out a version from the generic templated class.
The template <>
tells the compiler that this is a template function, but that there are no template parameters (since in this case, we’re explicitly specifying all of the types). Some compilers may allow you to omit this, but it’s correct to include it.
As a result, when we rerun the above program, it will print:
5
6.700000e+000
Another example
However, perhaps surprisingly, the above specialized destructor won’t compile. This is because a specialized function must specialize an explicit function (not one that the compiler is providing a default for). Since we didn’t define a destructor in Storage, the compiler is providing a default destructor for us, and thus we can’t provide a specialization. To solve this issue, we must explicitly define a destructor in Storage. Here’s the full code:
#include <iostream>
#include <string>
template <typename T>
class Storage
{
private:
T m_value{};
public:
Storage(T value)
: m_value{ value }
{
}
~Storage() {}; // need an explicitly defined destructor to specialize
void print()
{
std::cout << m_value << '\n';
}
};
template <>
Storage<char*>::Storage(char* const value)
{
if (!value)
return;
// Figure out how long the string in value is
int length{ 0 };
while (value[length] != '\0')
++length;
++length; // +1 to account for null terminator
// Allocate memory to hold the value string
m_value = new char[length];
// Copy the actual value string into the m_value memory we just allocated
for (int count = 0; count < length; ++count)
m_value[count] = value[count];
}
template <>
Storage<char*>::~Storage()
{
delete[] m_value;
}
int main()
{
// Dynamically allocate a temporary string
std::string s;
// Ask user for their name
std::cout << "Enter your name: ";
std::cin >> s;
// Store the name
Storage<char*> storage(s.data());
storage.print(); // Prints our name
s.clear(); // clear the std::string
storage.print(); // Prints our name
}
Although the above examples have all used member functions, you can also specialize non-member template functions in the same way.
19.4 — Class template specialization
template <typename T>
class Storage8
{
private:
T m_array[8];
public:
void set(int index, const T& value)
{
m_array[index] = value;
}
const T& get(int index) const
{
return m_array[index];
}
};
Class template specialization
// Requires the Storage8 type definition from above
#include <cstdint>
template <> // the following is a template class with no templated parameters
class Storage8<bool> // we're specializing Storage8 for bool
{
// What follows is just standard class implementation details
private:
std::uint8_t m_data{};
public:
void set(int index, bool value)
{
// Figure out which bit we're setting/unsetting
// This will put a 1 in the bit we're interested in turning on/off
auto mask{ 1 << index };
if (value) // If we're setting a bit
m_data |= mask; // use bitwise-or to turn that bit on
else // if we're turning a bit off
m_data &= ~mask; // bitwise-and the inverse mask to turn that bit off
}
bool get(int index)
{
// Figure out which bit we're getting
auto mask{ 1 << index };
// bitwise-and to get the value of the bit we're interested in
// Then implicit cast to boolean
return (m_data & mask);
}
};
It’s worth noting that keeping the public interface between your template class and all of the specializations similar is generally a good idea, as it makes them easier to use – however, it’s not strictly necessary.
19.5 — Partial template specialization
#include <iostream>
#include <cstring>
template <typename T, int size> // size is the expression parameter
class StaticArray
{
private:
// The expression parameter controls the size of the array
T m_array[size]{};
public:
T* getArray() { return m_array; }
T& operator[](int index)
{
return m_array[index];
}
};
template <typename T, int size>
void print(StaticArray<T, size>& array)
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count] << ' ';
}
// overload of print() function for partially specialized StaticArray<char, size>
template <int size>
void print(StaticArray<char, size>& array)
{
for (int count{ 0 }; count < size; ++count)
std::cout << array[count];
}
int main()
{
// Declare an char array of size 14
StaticArray<char, 14> char14{};
std::strcpy(char14.getArray(), "Hello, world!");
// Print the array
print(char14);
std::cout << ' ';
// Now declare an char array of size 12
StaticArray<char, 12> char12{};
std::strcpy(char12.getArray(), "Hello, mom!");
// Print the array
print(char12);
return 0;
}
This prints:
Hello, world! Hello, mom!
Just as we expect.
Partial template specialization can only be used with classes, not template functions (functions must be fully specialized). Our void print(StaticArray<char, size> &array)
example works because the print function is not partially specialized (it’s just an overloaded function using a class parameter that’s partially specialized).
Partial template specialization for member functions
Fortunately, there’s a workaround, by using a common base class:
#include <iostream>
template <typename T, int size> // size is the expression parameter
class StaticArray_Base
{
protected:
// The expression parameter controls the size of the array
T m_array[size]{};
public:
T* getArray() { return m_array; }
T& operator[](int index)
{
return m_array[index];
}
void print()
{
for (int i{ 0 }; i < size; ++i)
std::cout << m_array[i] << ' ';
std::cout << '\n';
}
virtual ~StaticArray_Base() = default;
};
template <typename T, int size> // size is the expression parameter
class StaticArray: public StaticArray_Base<T, size>
{
};
template <int size> // size is the expression parameter
class StaticArray<double, size>: public StaticArray_Base<double, size>
{
public:
void print()
{
for (int i{ 0 }; i < size; ++i)
std::cout << std::scientific << this->m_array[i] << ' ';
// note: The this-> prefix in the above line is needed.
// See https://stackoverflow.com/a/6592617 or https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members for more info on why.
std::cout << '\n';
}
};
int main()
{
// declare an integer array with room for 6 integers
StaticArray<int, 6> intArray{};
// Fill it up in order, then print it
for (int count{ 0 }; count < 6; ++count)
intArray[count] = count;
intArray.print();
// declare a double buffer with room for 4 doubles
StaticArray<double, 4> doubleArray{};
for (int count{ 0 }; count < 4; ++count)
doubleArray[count] = (4.0 + 0.1 * count);
doubleArray.print();
return 0;
}
This prints the same as above, but has significantly less duplicated code.
19.6 — Partial template specialization for pointers
#include <iostream>
#include <cstring>
// Our Storage class for non-pointers
template <typename T>
class Storage
{
private:
T m_value;
public:
Storage(T value)
: m_value { value }
{
}
~Storage()
{
}
void print() const
{
std::cout << m_value << '\n';
}
};
// Partial-specialization of Storage class for pointers
template <typename T>
class Storage<T*>
{
private:
T* m_value;
public:
Storage(T* value)
: m_value { new T { *value } } // this copies a single value, not an array
{
}
~Storage()
{
delete m_value;
}
void print() const
{
std::cout << *m_value << '\n';
}
};
// Full specialization of constructor for type char*
template <>
Storage<char*>::Storage(char* value)
{
// Figure out how long the string in value is
int length { 0 };
while (value[length] != '\0')
++length;
++length; // +1 to account for null terminator
// Allocate memory to hold the value string
m_value = new char[length];
// Copy the actual value string into the m_value memory we just allocated
for (int count = 0; count < length; ++count)
m_value[count] = value[count];
}
// Full specialization of destructor for type char*
template<>
Storage<char*>::~Storage()
{
delete[] m_value;
}
// Full specialization of print function for type char*
// Without this, printing a Storage<char*> would call Storage<T*>::print(), which only prints the first char
template<>
void Storage<char*>::print() const
{
std::cout << m_value;
}
int main()
{
// Declare a non-pointer Storage to show it works
Storage<int> myint { 5 };
myint.print();
// Declare a pointer Storage to show it works
int x { 7 };
Storage<int*> myintptr { &x };
// If myintptr did a pointer assignment on x,
// then changing x will change myintptr too
x = 9;
myintptr.print();
// Dynamically allocate a temporary string
char* name { new char[40]{ "Alex" } };
// Store the name
Storage<char*> myname { name };
// Delete the temporary string
delete[] name;
// Print out our name to prove we made a copy
myname.print();
}
This works as we expect:
5
7
Alex
Using partial template class specialization to create separate pointer and non-pointer implementations of a class is extremely useful when you want a class to handle both differently, but in a way that’s completely transparent to the end-user.
19.x — Chapter 19 comprehensive quiz
Template specialization can be used when we want to override the default behavior from the templated function or class for a specific type. If all types are overridden, this is called full specialization. Classes also support partial specialization, where only some of the templated parameters are specialized. Functions can not be partially specialized.