Once we have a template class or function, we can create specialized versions of that template. These specializations allow us to provide a different implementation for a template, based on the exact template arguments received.
These different implementations can include behavioral changes, performance optimizations, and more.
Template specialization gives us a great deal of additional flexibility. It is also a common technique that is used to make our types compatible with functionality implemented in libraries, including the standard library. We’ll see an example of this later in the lesson
In the following example, we have three types - Tree
, Rock
, and Monster
. We’d like to create a generic algorithm that works with all these types - for example, to render them to the screen.
Given they have different types, we’d need to create a function template to support this:
#include <iostream>
struct Tree {
std::string Description{"Tree"};
};
struct Rock {
std::string Description{"Rock"};
};
struct Monster {
std::string Name{"Monster"};
};
template <typename T>
void Render(T Object) {
std::cout << "Rendering a "
<< Object.Description << '\n';
}
int main() {
Render(Tree{});
Render(Rock{});
Render(Monster{});
}
Unfortunately, our Monster
type is slightly different from the others. Rather than having a Description
, its member variable is called Name
. Accordingly, we get a compilation error when our template generates a function that tries to access a member that doesn’t exist when T
is Monster
:
error C2039: 'Description': is not a member of 'Monster'
To get around this, we can specialize our function template such that, if its template argument is a Monster
, the function it generates has a different implementation. It looks like this:
#include <iostream>
struct Tree; struct Rock; struct Monster;
// General Template
template <typename T>
void Render(T Object) {
std::cout << "Rendering a "
<< Object.Description << '\n';
}
// Specialized Template
template <>
void Render<Monster>(Monster Object) {
std::cout << "Rendering a "
<< Object.Name << '\n';
}
int main() {
Render(Tree{});
Render(Rock{});
Render(Monster{});
}
Rendering a Tree
Rendering a Rock
Rendering a Monster
Template specialization deals with providing entirely different implements of our templates based on their parameters. Often, this is unnecessary.
Simple cases can often be solved by adding conditional logic to our generic template. rather than providing a complete, alternate implementation through specialization.
For example, the previous problem could be solved by adding an if constexpr
statement to our function template, as below:
#include <iostream>
struct Tree; struct Rock; struct Monster;
template <typename T>
void Render(T Object) {
if constexpr (std::same_as<T, Monster>) {
std::cout << "Rendering a "
<< Object.Name << '\n';
} else {
std::cout << "Rendering a "
<< Object.Description << '\n';
}
}
int main() {
Render<Tree>(Tree{});
Render<Rock>(Rock{});
Render<Monster>(Monster{});
}
Rendering a Tree
Rendering a Rock
Rendering a Monster
We cover if constexpr
, type traits and concepts like std::same_as
in dedicated lessons later in this chapter.
Specializing member function templates uses a very similar syntax to specializing free function templates. Our following example is very similar to the previous one, except we’ve moved the Render
template within a Renderer
class:
#include <iostream>
struct Tree; struct Rock; struct Monster;
class Renderer {
public:
// General template
template <typename T>
void Render(T Object) {
std::cout << "Rendering "
<< Object.Description << '\n';
};
// Specialized template
template <>
void Render<Monster>(Monster Object) {
std::cout << "Rendering "
<< Object.Name << '\n';
}
};
int main() {
Renderer A;
A.Render(Tree{});
A.Render(Rock{});
A.Render(Monster{});
}
Rendering A Tree
Rendering A Rock
Rendering A Monster
We also have the option of overloading a member function outside of the class declaration, if preferred. This is the most common technique in large projects, as it allows us to provide the overload alongside the type it is associated with.
For example, we can imagine Renderer
being a core system in our project, whilst Monster
is a type we want to make compatible with it via template specialization. In this scenario, we’d prefer the specialization be in the same files that define our Monster
type. We can implement that as follows:
// Monster.h
#include <string>
#include "Renderer.h"
struct Monster {
std::string Name{"A Monster"};
};
template <>
void Renderer::Render<Monster>(Monster Object) {
// Monster-specific rendering implementation
}
Class template specialization has the same use cases and syntax as the other forms of specialization we have seen. Below, we’ve replicated a similar pattern as above, except now our entire class is a template, rather than just a member function:
#include <iostream>
struct Tree; struct Rock; struct Monster;
// General Template
template <typename T>
class Renderable {
public:
void Render() {
std::cout << "Rendering "
<< Object.Description << '\n';
};
T Object;
};
// Specialized Template
template <>
class Renderable<Monster> {
public:
void Render() {
std::cout << "Rendering " <<
Object.Name << '\n';
};
Monster Object;
};
int main() {
Renderable SomeTree{Tree{}};
SomeTree.Render();
Renderable SomeRock{Rock{}};
SomeRock.Render();
Renderable SomeMonster{Monster{}};
SomeMonster.Render();
}
Rendering A Tree
Rendering A Rock
Rendering A Monster
We can alternatively provide member functions for a specific instantiation of our class template, without providing a replacement for the entire class.
In the following example, we have also moved the general definition of Render
outside the class body for clarity, but this is optional:
#include <iostream>
struct Tree; struct Rock; struct Monster;
template <typename T>
class Renderable {
public:
void Render();
T Object;
};
// General implementation of Render
template <typename T>
void Renderable<T>::Render() {
std::cout << "Rendering "
<< Object.Description << '\n';
}
// Specialized implementation of Render
template <>
void Renderable<Monster>::Render() {
std::cout << "Rendering " <<
Object.Name << '\n';
}
int main() {
Renderable SomeTree{Tree{}};
SomeTree.Render();
Renderable SomeRock{Rock{}};
SomeRock.Render();
Renderable SomeMonster{Monster{}};
SomeMonster.Render();
}
Rendering A Tree
Rendering A Rock
Rendering A Monster
Our previous examples of class template specialization specified the values of every argument in the template parameter list.
We can determine when this is the case because our parameter list will be empty:
template <>
void Renderable<Monster>::Render() {
// ...
}
These were examples of full specialization. However, 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.
In this example, we’ve updated our Renderable
class template to have two parameters - a typename
and an int
. Then, we’ve provided a partial specialization that specifies only one of those parameters.
As such, our specialization applies to any instantiation of our template that uses 1
as the Quantity
argument, regardless of what is provided for the typename T
:
#include <iostream>
struct Tree; struct Rock; struct Monster;
// General template
template <typename T, int Quantity>
class Renderable {
public:
void Render() {
std::cout << "Rendering " << Quantity << " "
<< Object.Description << "s\n";
};
T Object;
};
// Specialization that is used if Quantity is 1
template <typename T>
class Renderable<T, 1> {
public:
void Render() {
std::cout << "Rendering a single "
<< Object.Description << "\n";
};
T Object;
};
int main() {
Renderable<Tree, 1> OneTree{Tree{}};
OneTree.Render();
Renderable<Rock, 1> OneRock{Rock{}};
OneRock.Render();
Renderable<Tree, 10> SomeTrees{Tree{}};
SomeTrees.Render();
}
Rendering a single Tree
Rendering a single Rock
Rendering 10 Trees
In this example, we’ve partially specialized Renderable
for any instantiation that uses Monster
as the argument for T
, regardless of what Quantity
is used:
#include <iostream>
struct Tree; struct Rock; struct Monster;
class Renderable {/*...*/};
// Specialization that is used if T is Monster
template <int Quantity>
class Renderable<Monster, Quantity> {
public:
void Render() {
std::cout << "Rendering " << Quantity << " "
<< Object.Name << "s\n";
};
Monster Object;
};
int main() {
Renderable<Tree, 10> Trees{Tree{}};
Trees.Render();
Renderable<Monster, 10> Monsters{Monster{}};
Monsters.Render();
Renderable<Monster, 500> Army{Monster{}};
Army.Render();
}
Rendering 10 Trees
Rendering 10 Monsters
Rendering 500 Monsters
Of course, our specialization could provide values for both arguments. This would be another example of a full specialization:
template <>
class Renderable<Monster, 1> {
// ...
};
It’s not possible to specialize the template of a member function when the class itself is also a template. However, we can get equivalent behaviour by conventional function overloading.
Our following example has a member function called Render
that accepts a Monster
, and a template function that accepts any type.
Based on how overload resolution happens, which we cover in detail later in the chapter, the non-template function takes priority over the templated function. This gives us the same behaviour as if we had specialized the template function:
#include <iostream>
struct Tree; struct Rock; struct Monster;
template <int Quantity>
class Renderer {
public:
template <typename T>
void Render(T Object) {
std::cout << "Rendering " << Quantity << " "
<< Object.Description << "s\n";
};
void Render(Monster Object) {
std::cout << "Rendering " << Quantity << " "
<< Object.Name << "s\n";
};
};
int main() {
Renderer<10> A;
A.Render(Tree{});
A.Render(Rock{});
A.Render(Monster{});
}
Rendering 10 Trees
Rendering 10 Rocks
Rendering 10 Monsters
We cover overload resolution in more detail later in this chapter.
Below, we have moved the definition of both our template function and our member function outside of the class declaration, and included two additional examples:
#include <iostream>
struct Tree; struct Rock; struct Monster;
template <int Quantity>
class Renderer {
public:
template <typename T>
void Render(T Object);
void Render(Monster Object);
};
// Generic implementation
template <int Quantity>
template <typename T>
void Renderer<Quantity>::Render(T Object) {
std::cout << "Rendering " << Quantity << " "
<< Object.Description << "s\n";
}
// Specialisation for Quantity = 1
template <>
template <typename T>
void Renderer<1>::Render(T Object) {
std::cout << "Rendering a single "
<< Object.Description << '\n';
}
// Overload for T = Monster
template <int Quantity>
void Renderer<Quantity>::Render(Monster Object) {
std::cout << "Rendering " << Quantity << " "
<< Object.Name << "s\n";
}
// Specialization for Quantity = 1
// And Overloaded for T = Monster
template <>
void Renderer<1>::Render(Monster Object) {
std::cout << "Rendering a single "
<< Object.Name << '\n';
}
int main() {
Renderer<10> A;
A.Render(Tree{});
A.Render(Rock{});
A.Render(Monster{});
Renderer<1> B;
B.Render(Tree{});
B.Render(Rock{});
B.Render(Monster{});
}
Rendering 10 Trees
Rendering 10 Rocks
Rendering 10 Monsters
Rendering a single Tree
Rendering a single Rock
Rendering a single Monster
One of the main practical use cases for template specialization is to enable integration between different code bases. For example, we might want to use a library of code created by some other developer.
One way they might decide to enable this integration could involve the library developer providing templates, and us specializing those templates for our types.
For example, let’s imagine we’re using a library that does some processing of an object, and part of that process requires the object’s name.
The library author doesn’t know how to get that name, because it doesn’t know what types it will be dealing with. Therefore, they provide a templated function called GetName()
:
// SomeLibrary.h
#pragma once
#include <iostream>
namespace SomeLibrary{
template <typename T>
std::string GetName(T& Obj){
return Obj.GetName();
}
template <typename T>
void Process(T& Obj){
std::cout << "Processing " << GetName(Obj);
//...
std::cout << "\nDone!";
}
}
In our code, for any type we want to be compatible with this library, we would specialize the GetName()
function, telling the library how to get our objects’ names:
// Player.h
#pragma once
#include <string>
#include "SomeLibrary.h"
struct Player {
std::string Name;
};
namespace SomeLibrary {
template <>
std::string GetName<Player>(Player& P) {
return P.Name;
}
}
Now, our objects can work elegantly with the library, without the library knowing anything about our code:
#include <SomeLibrary.h>
#include "Player.h"
int main() {
Player PlayerOne{"Anna"};
SomeLibrary::Process(PlayerOne);
}
Processing Anna
Done!
The standard library also uses this approach. For example:
std::format()
and std::print()
, we’d need to specialise the std::formatter
templatestd::set
and std::map
, we’d need to specialise the std::hash
templateWe’ll introduce both of these topics in detail later in the course.
This lesson covers template specialization in C++, a technique that allows creating specialized versions of function and class templates for specific types. Key takeaways include:
A practical guide to template specialization in C++ covering full and partial specialization, and the scenarios where they’re useful
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.