Template Specialization

A practical guide to template specialization in C++ covering full and partial specialization, and the scenarios where they’re useful
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

21.jpg
Ryan McCombe
Ryan McCombe
Posted

Once we have a template class or function, we have the ability to create specialized versions of that template. These specializations allow us to provide a different implementation for a template for a given set of template arguments.

These different implementations can include behavioral changes, performance optimizations, and more.

Template specialization also allows us to integrate third-party libraries or other code that requires specific interfaces or behaviors, without modifying our own code. We’ll see an example of this later in the lesson

Once we have a template class or function, we have the ability to create specialized versions of that template. These specializations allow us to provide a different implementation for a template for a given set of template arguments.

These different implementations can include behavioral changes, performance optimizations, and more.

Template specialization also allows us to integrate third-party libraries or other code that requires specific interfaces or behaviors, without modifying our own code. We’ll see an example of this later in the lesson

Class Template Specialization

Let's imagine we have a simple template class that holds another object. The type of that other object is a template type:

template <typename T>
class ObjectHolder {
public:
  ObjectHolder(T Object) : Object{Object}{}
  T Object;
};

To create a specialization of this template to work with int objects, the syntax would look like this:

template <>
class ObjectHolder<int> {
public:
  ObjectHolder(int Object) : Object{Object}{
    std::cout << "Oh great, an integer\n";
  }

  int Object;
};

To summarise, we maintain the template syntax above the class, even if it no longer has any parameters.

We then specify the specialized type between chevrons < and > after our class name.

Then, anywhere in our class where we previously used our template type, we now use our known, specialized type - int, in this case.

Now, when we instantiate this template with an int, the specialized template will be used:

#include <iostream>

template <typename T>
class ObjectHolder {
public:
  ObjectHolder(T Object) : Object{Object}{}
  T Object;
};

template <>
class ObjectHolder<int> {
public:
  ObjectHolder(int Object) : Object{Object}{
    std::cout << "Oh great, an integer: ";
  }

  int Object;
};

int main(){
  ObjectHolder FloatContainer{0.1f};
  std::cout << FloatContainer.Object << '\n';

  ObjectHolder DoubleContainer{0.5};
  std::cout << DoubleContainer.Object << '\n';

  ObjectHolder IntContainer{1};
  std::cout << IntContainer.Object << '\n';
}
0.1
0.5
Oh great, an integer: 1

A template can have many specializations as we need:

template <typename T>
class ObjectHolder {};

template <>
class ObjectHolder<int> {};

template <>
class ObjectHolder<float> {};

However, before we go too far down that route, I’d recommend finishing the remaining lessons in this chapter.

They cover type traits and concepts, which give us lots of additional options, which may include better solutions to our specific use cases than lots of specialized templates.

Partial Class Template Specialization

When we specialize a template, we don’t need to specify every template parameter. We can specify only a subset of them, using a technique called partial template specialization.

Let's imagine we have the following class, which has two template parameters:

template <typename TFirst, typename TSecond>
class SomeClass {};

To partially specialize this template for when the first type is an int, the syntax looks like this:

template <typename TSecond>
class SomeClass<int, TSecond> {};

This is similar to our previous example. Firstly, we removed the template parameter(s) we’re using in our specialization get removed from the template list.

We then specialized the class in the normal way using the < and > syntax, mixing specialization parameters and template parameters as needed.

In the following example, we update our earlier ObjectHolder class to instead store a std::array of objects. The size of the array is specified as a second, non-type template parameter:

#include <array>

template <typename T, size_t Size>
class ObjectHolder {
public:
  ObjectHolder() : Array{
    std::array<T, Size>{}}{}

  std::array<T, Size> Array;
};

We can partially specialize this class template, such that we provide a specific implementation when the type is int, but the Size remains a template parameter:

#include <iostream>
#include <array>

template <typename T, size_t Size>
class ObjectHolder {
public:
  ObjectHolder() : Array{
    std::array<T, Size>{}}{}

  std::array<T, Size> Array;
};

template <size_t Size>
class ObjectHolder<int, Size> {
public:
  ObjectHolder() : Array{
    std::array<int, Size>{}}{
    std::cout << "Oh great, more integers: ";
  }

  std::array<int, Size> Array;
};

int main(){
  ObjectHolder<float, 1> Floats;
  std::cout << Floats.Array.size() << '\n';

  ObjectHolder<double, 3> Doubles;
  std::cout << Doubles.Array.size() << '\n';

  ObjectHolder<int, 5> Ints;
  std::cout << Ints.Array.size() << '\n';
}
1
3
Oh great, more integers 5

Function Template Specialization

We can also specialize function templates, using similar syntax:

#include <iostream>

template <typename T>
auto Subtract(T x, T y){
  std::cout << "Unspecialized\n";
  return x - y;
}

template <>
auto Subtract<float>(float x, float y){
  std::cout << "Specialized - float\n";
  return x - y;
}

template <>
auto Subtract<double>(double x, double y){
  std::cout << "Specialized - double\n";
  return x - y;
}

int main(){
  Subtract(2, 1); // int, int
  Subtract(2.0f, 1.0f); // float, float
  Subtract(2.0, 1.0); // double, double
}
Unspecialized
Specialized - float
Specialized - double

When a template function has multiple template parameters, the syntax looks like this:

#include <iostream>

template <typename TFirst, typename TSecond>
auto Subtract(TFirst x, TSecond y){
  std::cout << "Unspecialized\n";
  return x - y;
}

template <>
auto Subtract<float, double>(float x, double y){
  std::cout << "Specialized - float, double\n";
  return x - y;
}

template <>
auto Subtract<int, double>(int x, double y){
  std::cout << "Specialized - int, double\n";
  return x - y;
}

int main(){
  Subtract(2.0f, 1.0f); // float, float
  Subtract(2.0, 1.0); // double, double
  Subtract(2.0, 1.0f); // double, float
  Subtract(2.0f, 1.0); // float, double
  Subtract(2.0f, 1.0); // int, double
}
Unspecialized
Unspecialized
Unspecialized
Specialized - float, double
Specialized - int, double

Partial Function Template Specialization

Function templates do not support partial specialization, but any use case where we might want such a feature is covered by static overloading instead.

Our beginner course introduced static overloading of regular functions, but the exact same concepts apply to template functions

When we make a function call, the compiler will first check if any regular function can serve that call. If no valid candidate is found, the compiler will then search for suitable template functions. When we don’t explicitly state the template we want our function call to use with the < and > syntax, the compiler will deduce the template to use based on this process:

  • Prioritise non-template functions over template functions
  • Prioritise template functions with fewer template parameters over those with more
  • Prioritise specialized template functions over unspecialized template functions

The following code example demonstrates these behaviors:

#include <iostream>

void Func(double x, double y){
  std::cout << "Regular Function Call\n";
}

template <typename T>
void Func(double x, T y){
  std::cout <<
  "Template A: One Param - double, T\n";
}

template <typename T>
void Func(T x, double y){
  std::cout <<
  "Template B: One Param - T, double\n";
}

template <>
void Func<int, int>(int x, int y){
  std::cout <<
  "Template C: Two Params (Specialized)\n";
}

template <typename TFirst, typename TSecond>
void Func(TFirst x, TSecond y){
  std::cout << "Template D: Two Params\n";
}

int main(){
  std::cout << "Template Argument Deduction\n";
  Func(1.0, 2.0);   // Regular function call
  Func(1.0, 2.0f);  // One-parameter template
  Func(1.0f, 2.0);  // One-parameter template
  Func(1, 2);       // Two-parameter specialized
  Func(1.0f, 2.0f); // Two-parameter template

  std::cout << "\nExplicit Calls\n";
   // Two-parameter template
  Func<double, double>(2.0, 1.0);

  // Two-parameter specialized template
  Func<int, int>(2.0, 1.0); 
}
Template Argument Deduction
Regular Function Call
Template A: One Param - double, T
Template B: One Param - T, double
Template C: Two Params (Specialized)
Template D: Two Params

Explicit Calls
Template C: Two Params
Template D: Two Params (Specialized)

Note, when overloading template functions, the same rules apply with regard to ambiguity. If the compiler has two viable candidates with the same priority, it will throw an error.

The following example shows this, as there are two function templates that can handle our function call. Both functions have the same priority (ie, they are unspecialized templates with the same number of template arguments), so the compiler is unclear on which template it should use::

template <typename T>
auto Subtract(double x, T y){
  return x - y;
}

template <typename T>
auto Subtract(T x, double y){
  return x - y;
}

int main(){
  Subtract(2.0, 1.0);
}
'Subtract': ambiguous call to overloaded function
could be 'auto Subtract<double>(T,double)'
or       'auto Subtract<double>(double,T)'
while trying to match the argument list '(double, double)'

Class Method Specialization

Class methods can also be templates:

class Logging {
public:
  template <typename T>
  static void Log(T Object){
    std::cout << Object << '\n';
  }
};

When we want to provide a specialization for a class method, we should do it outside of the class declaration. To provide a specialization for the above method when using an int, our implementation would look something like this:

#include <iostream>

class Logging {
public:
  template <typename T>
  static void Log(T Object){
    std::cout << Object << '\n';
  }
};

template <>
void Logging::Log<int>(int Object){
  std::cout << "Another int: ";
  std::cout << Object << '\n';
}

int main(){
  Logging::Log(0.5);
  Logging::Log(2.5f);
  Logging::Log(5);
}
0.5
2.5
Another int: 5

Template Specialization with Libraries

A common use case for template specialization comes up when we’re integrating third-party libraries into our projects. For example, later in this course, we’ll use libraries that allow us to convert our objects into data formats that let them be saved on our hard drive, or transmitted over the internet.

Inherently, a library made by someone else is not going to know about the custom types we’re using in our application, so they’re likely to make use of template classes and functions.

Below, we create a library that has a templated process function, that we can imagine does some work with our objects.

// SomeLibrary.h
#pragma once
#include <iostream>

namespace SomeLibrary{
  template <typename T>
  void Process(T Object){
    std::cout << "Processing "
      << Object.GetDescription();

    // ...do work
    std::cout << "\nDone!";
  }
}

Being a templated function, we can call it with an instance of our custom type:

// main.cpp
#include <SomeLibrary.h>
#include "Character.h"

int main(){
  Character Player;
  SomeLibrary::Process(Player);
}

However, our type is not yet compatible with the library, so we’ll get an error:

'GetDescription': is not a member of 'Character'

When using (or writing) a library, there are three main ways we can allow custom types to be compatible.

Option 1 - Class Requirements

The first is to simply require these types to implement certain methods.

In this case, the library can document that, for any type to be compatible, it needs to implement a GetDescription() method. That method must return something that can be streamed to the terminal:

// Character.h
#pragma once

class Character {
public:
  std::string GetDescription() const{
    return "a Character object";
  }

  // ...
};

This approach is the most common, but it does have some problems. First, it is modifying the public interface of our class in a way that we probably don’t want. Later in the course, we’ll see a way to eliminate this drawback using the friend keyword, but even then it will still require us to modify our class.

Another problem is that it’s just not clear from this code why the GetDescription() method exists, or that it is there for compatibility with an external library. The library is not referenced anywhere in the class.

If we later remove or replace the library, we’ll not get any compiler feedback to tell us we can now also remove methods like this from our classes. Inevitably, this leads to a lot of useless code clogging up our project.

Option 2 - Inheritance

Another option libraries can adopt is to provide their own class, which implements all the requirements. Then, for any class to be compatible with the library, it just needs to inherit from that library class:

// Character.h
#pragma once
#include <SomeLibrary.h>

class Character : public SomeLibrary::Object {
  // ...
};

This approach makes the dependency clear, but it has some problems. Quite often, the information the library needs to accomplish its task will be stored in the derived class, thereby not directly accessible to the base class.

As such, this approach generally requires us to write some additional code to call methods on the base class, depending on what the library requires.

Option 3 - Specialization

Finally, the third option is to use template specialization. Any time the library needs to retrieve information from an object of a templated type, it can separate that retrieval into a standalone template function, such as the SomeLibrary::GetDescription example below:

// SomeLibrary.h
#pragma once
#include <iostream>

namespace SomeLibrary{
  template <typename T>
  std::string GetDescription(const T& Object){
    return Object.GetDescription();
  }

  template <typename T>
  void Process(T Object){
    std::cout << "Processing "
      << GetDescription(Object);

    // ...do work
    std::cout << "\nDone!";
  }
}

Now, for our type to be compatible with the library, it just provides a specialization for that template function:

// Character.h
#pragma once

class Character {
  // ...
};

namespace SomeLibrary{
  template <>
  std::string GetDescription<Character>
  (const Character& Object){
    return "a Character object";
  }
}

This solves the problem in a way that is explicit about why the code exists, and also doesn’t require any modification to our class.

Processing a Character object
Done!

Similarly, if we ever remove or replace the library, or this requirement ever changes, the compiler will throw a helpful error. This alerts us to an area where we may need to make changes, or perhaps an area where we can just delete some code that is no longer necessary, keeping our project clean:

'std::string SomeLibrary::GetDescription<Character>(const Character &)'
is not a specialization of a function template

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Templates
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Type Traits

A detailed and practical tutorial for type traits in modern C++ covering how we can use them, and examples of the most common use cases
DreamShaper_v7_fantasy_female_pirate_Sidecut_hair_black_clothi_0.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved