C++ Template Specialization: Full vs Partial Explained
Full specialisation gives one type its own C++ class template; partial specialisation matches a pattern like all pointers. Traits, and why functions overload.
- Course: C++ study plan
- Module: Templates and generic programming
- Kind: Lesson
- Reading time: 13 min
- Runtime: Clang 18 · C++20
What is template specialisation in C++?
Template specialisation is a separate definition of a template for particular arguments. A full specialisation, template <> struct Describe<bool>, replaces the primary template for exactly one type; a partial specialisation, template <typename T> struct Describe<T*>, matches a pattern and stays a template. A specialisation inherits none of the primary's members. std::vector<bool> is the standard library's best-known full specialisation.
Lesson
A template is a rule that applies to every type; a specialisation is the exception written next to it. std::vector<bool> packs bits where every other std::vector<T> stores elements, std::hash<std::string> hashes characters where the primary template has no body at all, and every std::is_integral<T> is a specialisation that says true for a handful of types. This lesson covers full specialisation (one exact type, such as const char* or bool), partial specialisation (a pattern, such as every pointer or every std::pair<T, T>), the traits style that the next lessons build on, and the reason a function template is overloaded rather than specialised.
Full specialisation
#include <iostream>
#include <string>
template <typename T>
struct Describe { // the primary template
static void print(const T& v) { std::cout << "[value] " << v << '\n'; }
};
template <> // no parameters left to declare
struct Describe<bool> {
static void print(bool v) { std::cout << "[bool] " << (v ? "true" : "false") << '\n'; }
};
template <>
struct Describe<const char*> {
static void print(const char* v) { std::cout << "[C string] \"" << v << "\"\n"; }
};
template <>
struct Describe<std::string> {
static void print(const std::string& v) { std::cout << "[string] \"" << v << "\"\n"; }
};
template <> with an empty parameter list followed by the name with its arguments filled in — Describe<bool> — declares an explicit (full) specialisation. When the compiler needs Describe<bool> it uses that class and never instantiates the primary for bool. The specialisation is a separate class: it inherits nothing from the primary, so if the primary has five members and the specialisation defines two, the other three do not exist for bool. Keep the interface the same on purpose; the compiler does not make you.
A specialisation must be declared before the first use that would instantiate the primary. Use Describe<bool> on line 10 and specialise it on line 30 and GCC stops with specialization of 'Describe<bool>' after instantiation. Headers put the specialisations right after the primary for this reason.
You can also specialise a single member instead of the whole class — template <> void Describe<int>::print(const int& v) { … } — which keeps every other member from the primary. It is the neater choice when only one function differs.
Partial specialisation
A full specialisation names one type; a partial specialisation names a pattern and stays a template:
#include <utility>
#include <vector>
template <typename T>
struct Describe<T*> { // every pointer type
static void print(T* p) {
std::cout << "[pointer to] ";
if (p) Describe<T>::print(*p);
else std::cout << "null\n";
}
};
template <typename T>
struct Describe<std::vector<T>> { // every vector, of anything
static void print(const std::vector<T>& v) {
std::cout << "[vector of " << v.size() << "]\n";
for (const T& item : v) Describe<T>::print(item);
}
};
template <typename A, typename B>
struct Describe<std::pair<A, B>> { // any pair
static void print(const std::pair<A, B>& p) { std::cout << "[pair]\n"; }
};
template <typename T>
struct Describe<std::pair<T, T>> { // both halves the same type
static void print(const std::pair<T, T>& p) { std::cout << "[homogeneous pair]\n"; }
};
The parameter list after template is whatever is still free — T for pointers, A and B for a general pair — and the argument list after the name is the pattern to match. Describe<int*> matches Describe<T*> with T = int; Describe<std::vector<std::string>> matches the vector pattern and recurses into Describe<std::string>, which is the full specialisation above. Because the recursion goes through the template again, Describe<std::vector<std::vector<int>>> works with no further code.
When several partial specialisations match, the compiler picks the most specialised — the one whose pattern could be matched by the other but not the reverse. std::pair<int, int> matches both pair patterns; pair<T, T> is more specialised than pair<A, B> (every pair<T, T> is a pair<A, B>, not vice versa), so it wins. Two patterns where neither is more specialised make the use ambiguous and the compiler says so. The library uses this machinery in std::unique_ptr<T[]>, a partial specialisation for arrays that adds operator[] and calls delete[].
Traits: specialisation as a lookup table
template <typename T> struct TypeName { static constexpr const char* value = "unknown"; };
template <> struct TypeName<int> { static constexpr const char* value = "int"; };
template <> struct TypeName<double> { static constexpr const char* value = "double"; };
template <> struct TypeName<std::string> { static constexpr const char* value = "std::string"; };
template <typename T>
constexpr const char* type_name_v = TypeName<T>::value; // a variable template
std::cout << type_name_v<double> << '\n'; // double
A trait is a class template whose members are compile-time facts about T, filled in by specialisation. The whole of <type_traits> (lesson 6) is this pattern: std::is_integral<T> derives from std::false_type, and specialisations for int, long, char, bool and their relatives derive from std::true_type; std::is_pointer<T*> is a partial specialisation. The _v variable template is the C++17 convenience so you write std::is_integral_v<T> instead of std::is_integral<T>::value.
Why a trait instead of typeid(T).name()? Because name() returns the mangled name — GCC and Clang print i for int and PKc for const char*, MSVC prints something else again — and it is never a string you want in output. A trait gives you the words you chose.
Function templates: overload, do not specialise
A function template can be fully specialised, but never partially:
template <typename T> void show(const T& v) { std::cout << v << '\n'; }
template <typename T> void show<std::vector<T>>(const std::vector<T>& v); // error: function templates
// do not partially specialise
template <typename T> void show(const std::vector<T>& v); // a second template: legal, an overload
template <> void show<const char*>(const char* const& v); // legal full specialisation, but a trap
void show(const char* v); // the overload: what you want
The trap is that a full specialisation does not take part in overload resolution. The compiler first picks the best primary template or non-template for the call; only if the chosen primary has a matching specialisation is that used. The classic demonstration: template <class T> void f(T); specialised as template <> void f(int*); and then a second template template <class T> void f(T*); added later. f(p) with an int* chooses the second primary, because T* is the better match, and the specialisation of the first is never considered. An ordinary overload void f(int*) would have won outright. Overloads are the tool for functions; specialisation is the tool for class templates.
Where each tool fits
| Need | Tool |
|---|---|
A different representation for one type (std::vector<bool>) | Full specialisation of the class |
Extra members for a family of types (unique_ptr<T[]>) | Partial specialisation |
A compile-time fact per type (is_integral, TypeName) | Trait: primary default plus specialisations |
| One function that treats one type differently | A non-template overload |
| A few lines that differ inside one body | if constexpr (lesson 6) |
| "Only types that can do X" | A concept (lesson 5) |
Pitfalls
| Mistake | What happens |
|---|---|
| Specialising after the first use | specialization … after instantiation error. |
| Expecting the full specialisation to keep the primary's members | It has only what it declares. |
template <typename T> void f<T*>(T*) | Partial specialisation of a function: syntax error. Write an overload. |
Relying on template <> void f<int*>(int*) to win | It is not an overload candidate; a better-matching primary hides it. |
| Two partial specialisations, neither more specialised | Ambiguous instantiation error at the use. |
Printing typeid(T).name() | Mangled, compiler-specific text. |
template <> struct TypeName { … } (arguments missing) | Not a specialisation; redefinition error. |
Key takeaways
template <> struct X<Type>replaces the primary for exactly that type; it inherits none of its members.template <typename T> struct X<T*>matches a pattern and stays a template; the most specialised match wins.- Traits are class templates whose specialisations record a fact per type;
<type_traits>is built this way. - Declare specialisations before the first use, in the header, next to the primary.
- Function templates do not partially specialise, and a full specialisation is not an overload candidate: overload instead.
- Use your own tag strings, never
typeid(T).name(), for anything printed.
Common questions
What is the difference between full and partial template specialisation?
A full, or explicit, specialisation fixes every template argument — template <> followed by X<int> — and describes one exact type. A partial specialisation fixes only a pattern, such as X<T*> for all pointers or X<std::pair<T, T>> for pairs of one type, and still has template parameters. When several partial specialisations match, the most specialised one wins.
Can a function template be partially specialised?
No. Partial specialisation applies to class templates, not function templates: template <typename T> void f<T*>(T*) is a compile error. Write a second overload instead. A function template can be fully specialised, but a full specialisation does not take part in overload resolution, so an ordinary overload is the better tool.
Why should you overload a function template instead of specialising it?
A full specialisation is not an overload candidate. The compiler first chooses the best primary template or non-template function and only then looks at that primary's specialisations, so a better-matching second template can hide your specialisation entirely. A plain non-template overload such as void show(const char*) takes part in overload resolution and wins exact-match ties.
What is a type trait in C++?
A type trait is a class template whose members record a compile-time fact about a type, filled in by specialisation: the primary template gives the default and specialisations give the exceptions. <type_traits> is built this way — std::is_integral<T> is false by default and true for int, char, bool and the other integer types.
Why does typeid(T).name() print strange names?
typeid(T).name() returns an implementation-defined, usually mangled name: GCC and Clang print i for int and PKc for const char*, and MSVC prints something else again. For readable output, define a trait such as TypeName<T> whose specialisations hold the words you choose.
Exercises
Describe<T> with full specialisations
Write a class template Describe<T> with one static member print. The primary template prints [value] <v>. Add three full specialisations: Describe<bool> prints [bool] true or [bool] false; Describe<const char*> prints [C string] "<v>"; Describe<std::string> prints [string] "<v>". The tags are yours — never print typeid(T).name().
Read n, then n lines of <kind> <token> with <kind> one of int, double, bool (token 0, 1, true or false), string and cstr. For cstr call Describe<const char*>::print with the token's c_str(), so the pointer specialisation is what runs. Doubles print with the stream's default formatting (2.5 prints as 2.5).
Input: n, then n lines of kind token. Output: one line per input line.
5
int 42
double 2.5
bool 1
string hello
cstr hello
prints
[value] 42
[value] 2.5
[bool] true
[string] "hello"
[C string] "hello"Partial specialisations for vectors and pairs
Write a class template Describe<T> whose static text(const T&) returns a std::string, so specialisations can compose. The primary returns [value] <v>; the full specialisation Describe<std::string> returns [string] "<v>". Then add three partial specialisations:
Describe<std::vector<T>>returns[vector of <k>: <e1>, <e2>, …]with each element described byDescribe<T>::text— or[vector of 0: empty].Describe<std::pair<A, B>>returns[pair: <a> | <b>].Describe<std::pair<T, T>>returns[same pair: <a> | <b>]— the more specialised pattern, chosen whenever both halves have the same type.
Read n, then n lines: ints <k> <v…> (a std::vector<int>), words <k> <w…> (a std::vector<std::string>), pair <int> <word> (a std::pair<int, std::string>), same <int> <int> (a std::pair<int, int>), pairs <k> <a1> <b1> … (a std::vector<std::pair<int, int>>). Print the description of each.
Input: n, then n lines. Output: one line per input line.
5
ints 3 1 2 3
words 2 pear apple
pair 7 seven
same 4 5
pairs 2 1 2 3 4
prints
[vector of 3: [value] 1, [value] 2, [value] 3]
[vector of 2: [string] "pear", [string] "apple"]
[pair: [value] 7 | [string] "seven"]
[same pair: [value] 4 | [value] 5]
[vector of 2: [same pair: [value] 1 | [value] 2], [same pair: [value] 3 | [value] 4]]In this module: Templates and generic programming
- Function templates — one definition, every type
- Class templates — Stack<T>, Pair<A, B> and deduction
- Specialisation — when one type needs its own version (this lesson)
- Non-type and variadic templates — numbers and packs as parameters
- Concepts and constraints — saying what T must be
- Type traits and metaprogramming — computing with types
- Checkpoint — Templates and generic programming
← Class templates — Stack<T>, Pair<A, B> and deduction · Non-type and variadic templates — numbers and packs as parameters →