Template specialisation
A template describes the general case, and sometimes one type deserves a different implementation. A full specialisation writes that implementation for exactly one set of arguments, and the compiler picks it whenever the arguments match.
The standard library specialises std::vector<bool> to pack bits and std::hash for every type it can hash. This lesson writes the three forms: a full class specialisation, a function specialisation and a partial specialisation that still leaves a parameter open.
A full specialisation
template <> class Box<const char*> with empty angle brackets is a full specialisation: it replaces the general Box for one exact type. The compiler chooses it when the argument is exactly const char*, and the general template for everything else.
The program stores a C string in a Box and prints through the specialised print(), which shows the pointer rather than the text. The general version would print the address, so the specialisation exists to print the characters.
A specialisation must be declared before the first use that would instantiate the general template. The last section shows the error when it comes too late.
The specialisation does not have to look like the general template. It can have different data members and different member functions, as long as it provides the operations the callers use; the compiler only checks the calls that are made.
#include <iostream>
template <typename T>
class Box
{
public:
explicit Box(T value) : value_(value) {}
void print() const
{
std::cout << value_ << '\n';
}
private:
T value_;
};
template <>
class Box<const char*>
{
public:
explicit Box(const char* value) : value_(value) {}
void print() const
{
std::cout << "text: " << value_ << '\n';
}
private:
const char* value_;
};
int main()
{
Box<int> number{42};
Box<const char*> text{"hello"};
number.print();
text.print();
}
g++ -std=c++23 -Wall -Wextra full.cpp -o full
./full
42
text: hello
Specialising a function
A function template is specialised with the same empty angle brackets and the exact parameter types. The general version handles arithmetic, and the specialisation prints a message for const char*.
The program calls show with an int and with a string. The int goes through the template, the string through the specialisation, and the two output lines differ.
Prefer an ordinary overload to a function specialisation where you can: overloads take part in normal overload resolution, while a specialisation only applies when the template would be instantiated with exactly those arguments. The next lesson's tag dispatch is the other standard alternative.
#include <iostream>
template <typename T>
void show(T value)
{
std::cout << "value " << value << '\n';
}
template <>
void show<const char*>(const char* value)
{
std::cout << "text " << value << '\n';
}
int main()
{
show(3);
show("hello");
}
g++ -std=c++23 -Wall -Wextra function.cpp -o function_spec
./function_spec
value 3
text hello
A partial specialisation
A partial specialisation fixes part of the parameter list and leaves the rest open. template <typename T> class Box<T*> matches every pointer type and still has a T to work with.
The program stores an int* and a double* in boxes and prints the pointed-to value through the partial specialisation. The general template is still used for the plain int box.
Partial specialisation exists for class templates only; a function template cannot be partially specialised and uses overloading instead. That asymmetry is one of the reasons the standard library puts traits in class templates.
The partial specialisation also shows why the general template keeps its body: Box<int> still uses it, and only the pointer forms are replaced.
#include <iostream>
template <typename T>
class Box
{
public:
explicit Box(T value) : value_(value) {}
void print() const
{
std::cout << "value " << value_ << '\n';
}
private:
T value_;
};
template <typename T>
class Box<T*>
{
public:
explicit Box(T* value) : value_(value) {}
void print() const
{
std::cout << "pointer to " << *value_ << '\n';
}
private:
T* value_;
};
int main()
{
int number = 7;
double real = 2.5;
Box<int> plain{3};
Box<int*> pointer{&number};
Box<double*> real_pointer{&real};
plain.print();
pointer.print();
real_pointer.print();
}
g++ -std=c++23 -Wall -Wextra partial.cpp -o partial_spec
./partial_spec
value 3
pointer to 7
pointer to 2.5
A trait built from a specialisation
The standard type traits are class templates with specialisations. A trait that answers a question about a type is a template with a bool value, true in the general case and false in a specialisation, or the other way round.
The program's is_pointer trait is true only for the pointer specialisation. The static_assert lines check both answers at compile time, and the printed value shows the same answer at run time.
This is the technique behind lesson 042: a primary template, one or more specialisations, and a _v helper that reads the value.
#include <iostream>
template <typename T>
struct is_pointer
{
static constexpr bool value = false;
};
template <typename T>
struct is_pointer<T*>
{
static constexpr bool value = true;
};
template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
int main()
{
static_assert(!is_pointer_v<int>);
static_assert(is_pointer_v<const char*>);
std::cout << is_pointer_v<int> << ' ' << is_pointer_v<double*> << '\n';
}
g++ -std=c++23 -Wall -Wextra trait.cpp -o trait
./trait
0 1
A specialisation after the first use
Once the compiler has instantiated the general template for a type, a later specialisation of that type is too late. The program below uses Box<int> in main and declares the specialisation afterwards, and the compiler rejects the declaration.
The message says the specialisation comes after instantiation, and the fix is to move it above every use, usually into the header where the general template lives.
The same rule applies to function templates. A specialisation is not a new overload that can appear anywhere; it changes what an existing template produces, so it has to be visible before the template is used.
#include <iostream>
template <typename T>
class Box
{
public:
explicit Box(T value) : value_(value) {}
void print() const
{
std::cout << value_ << '\n';
}
private:
T value_;
};
int main()
{
Box<int> b{3};
b.print();
}
template <>
class Box<int>
{
public:
explicit Box(int value) : value_(value) {}
void print() const
{
std::cout << "special " << value_ << '\n';
}
private:
int value_;
};
g++ -std=c++23 -Wall -Wextra late.cpp -o late
late.cpp:25:7: error: specialization of 'Box<int>' after instantiation
25 | class Box<int>
| ^~~~~~~~
late.cpp:25:7: error: redefinition of 'class Box<int>'
late.cpp:4:7: note: previous definition of 'class Box<int>'
4 | class Box
| ^~~
Exercises
- Add a full specialisation of Box for std::string that prints the length instead of the text.
-
Write a partial specialisation Box
for references and decide what print() should show. - Move the specialisation in late.cpp above main and run the program. Which line does main print now?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a full specialisation?
Answer
An implementation for one exact set of template arguments
The compiler prefers the specialisation when the arguments match it exactly.
-
What is a partial specialisation?
Answer
An implementation for a family of arguments, such as every pointer
The pattern
T*matches every pointer while leaving T open. -
Why does the standard library specialise
std::hashfor a type?Answer
So the type can be used as a key in an unordered container
The unordered containers look up a hash function, and the specialisation supplies one.
-
Where must a specialisation appear?
Answer
Before the first use that would instantiate the primary template
Once the primary template is instantiated for those arguments, a later specialisation is too late.
-
What does this program print?
#include <iostream> #include <string> template <typename T> struct printer { static T show(T value) { return value; } }; template <> struct printer<int> { static int show(int value) { return value; } }; int main() { std::cout << printer<int>::show(42) << ' ' << "text: " << printer<std::string>::show("hello"); }Answer
42 text: hello
The int specialisation returns 42, and the generic template answers for the string.
-
What does this program print?
#include <iostream> #include <string> template <typename T> void show(const T&) { std::cout << "value 3\n"; } template <> void show(const std::string&) { std::cout << "text hello\n"; } int main() { show(3); show(std::string{"hello"}); }Answer
value 3 / text hello
The generic template prints the first line, and the string specialisation prints the second.
-
What does this program print?
#include <iostream> template <typename T> struct describe { static void show(const T& value) { std::cout << "value " << value; } }; template <typename T> struct describe<T*> { static void show(T* p) { std::cout << "pointer to " << *p; } }; int main() { int number = 3; int seven = 7; double real = 2.5; describe<int>::show(number); std::cout << " / "; describe<int*>::show(&seven); std::cout << " / "; describe<double*>::show(&real); }Answer
value 3 / pointer to 7 / pointer to 2.5
The int uses the primary template and every pointer uses the partial specialisation.
-
What do the two trait results print?
Answer
0 1
The trait answers false for the int and true for the pointer, and the program prints the two answers.
-
This code is compiled. What does the compiler report?
template <typename T> struct Box {}; Box<int> first; template <> struct Box<int> { int value = 0; }; int main() { Box<int> second; return second.value; }Answer
specialization of 'Box<int>' after instantiation
The primary template was instantiated for int by the first variable, so the later specialisation is rejected.
-
Why does a function template need an ordinary function for a special case?
Answer
A function specialisation cannot be partially specialised
Only class templates support partial specialisation, so a function uses an overload instead.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev