C++ / TEMPLATES AND GENERIC PROGRAMMING
Partial specialisation of class templates
Match whole families of template arguments with partial specialisations, and predict which pattern the compiler picks when several of them match.
What you will learn
- Write `template <class T> struct S<T*>` to match a whole family of types at once
- Read a specialisation's argument list as a pattern the compiler deduces against
- Predict which body wins when several patterns match, using partial ordering
- Re-declare every member you need: a specialisation shares no code with the primary
Understanding Partial specialisation of class templates
A partial specialisation is not a second template that happens to share a name; it is an alternative body for the primary template, chosen by pattern matching on the arguments. In `template <class T> struct Describe<T*>`, the list after `template` declares the parameters the compiler may deduce, and the list after the name is the pattern they have to fill in. When code writes `Describe<int*>`, the compiler takes the argument `int*`, deduces `T = int` from the pattern `T*`, and instantiates that body. The primary template has to be declared first, because it is what fixes how many arguments the name takes and what `Describe<...>` means at all.
Matching is structural and exact, with none of the conversions a function call would permit. `T*` matches only a cv-unqualified pointer type, which is why `int* const` falls to the `T const` pattern with `T = int*` instead. Arrays do not decay, references stay references, and top-level cv-qualifiers are part of the type being matched. When several patterns match one instantiation the compiler ranks them by partial ordering — the pattern that matches a strict subset of what another matches wins — and if no single pattern is most specialised the instantiation is ambiguous and ill-formed; declaration order never enters into it.
Each specialisation is a complete, independent class. It inherits nothing from the primary, so anything you still want must be written again, and members you leave out simply do not exist for those arguments. That independence is the point: the specialised body can hold a different data member, a different layout, different functions, which is how the standard library builds `remove_const`, `is_pointer` and `tuple_element`. It is also why recursion works in the example below — `Describe<T*>` peels one layer off the type and hands the rest to `Describe<T>`, so four patterns describe an unbounded set of types.
<cstddef>
<iostream>
template <class T>
struct Describe { // primary template
static void print() { std::cout << "value"; }
};
template <class T>
struct Describe<T*> { // every pointer type
static void print() { std::cout << "pointer to "; Describe<T>::print(); }
};
template <class T>
struct Describe<T const> { // every top-level const type
static void print() { std::cout << "const "; Describe<T>::print(); }
};
template <class T, std::size_t N>
struct Describe<T[N]> { // every array of known bound
static void print() { std::cout << "array[" << N << "] of "; Describe<T>::print(); }
};
template <class T>
void show() {
Describe<T>::print();
std::cout << "\n";
}
int main() {
show<int>();
show<int*>();
show<const int*>();
show<int* const>();
show<double[4]>();
show<const char*[2]>();
}
A partial specialisation is a deduction pattern over template arguments, and each instantiation gets the most specialised pattern that matches it.
Worked examples
Which pattern wins
Four bodies compete for the same instantiation and partial ordering decides, independently of declaration order.
<iostream>
template <class T, class U>
struct Pick { static void who() { std::cout << "primary\n"; } };
template <class T>
struct Pick<T, int> { static void who() { std::cout << "<T, int>\n"; } };
template <class T, class U>
struct Pick<T*, U> { static void who() { std::cout << "<T*, U>\n"; } };
template <class T>
struct Pick<T*, int> { static void who() { std::cout << "<T*, int>\n"; } };
int main() {
Pick<double, char>::who();
Pick<double, int>::who();
Pick<double*, char>::who();
Pick<double*, int>::who();
}
Example explained
Line 1`Pick<double, char>` fits no pattern, so the primary template is the only candidate.
Line 2`Pick<double, int>` fits `<T, int>` but not `<T*, U>`, because `double` is not a pointer type.
Line 3`Pick<double*, int>` fits three patterns; `<T*, int>` wins because everything it matches the other two also match, and not the reverse.
Line 4Moving the three specialisations into any other order changes nothing: ranking happens per instantiation, not top to bottom.
Matching a non-type argument
A pattern can pin down a value argument and leave the type argument open, giving two unrelated class bodies.
<array>
<cstddef>
<iostream>
<vector>
template <class T, std::size_t N>
struct Storage {
std::array<T, N> data{};
static void kind() { std::cout << "array of " << N << "\n"; }
};
template <class T>
struct Storage<T, 0> {
std::vector<T> data;
static void kind() { std::cout << "heap vector\n"; }
};
int main() {
Storage<int, 4> fixed;
Storage<int, 0> dyn;
Storage<int, 4>::kind();
Storage<int, 0>::kind();
fixed.data[0] = 7;
dyn.data.push_back(7);
std::cout << fixed.data[0] << " " << dyn.data.size() << "\n";
}
Example explained
Line 1`Storage<T, 0>` fixes the second argument to the value 0 while `T` stays deducible.
Line 2The two bodies hold different members — `std::array<T, N>` versus `std::vector<T>` — and neither knows about the other.
Line 3`Storage<int, 4>` never instantiates the specialisation, so no vector exists in that object.
Line 4Only whole values can be matched; there is no pattern meaning "N greater than 4", which is why such splits use a sentinel like 0.
Detecting a class template
Naming a template-id in the pattern lets a trait recognise any instantiation of that template.
<iostream>
<memory>
<vector>
template <class T>
struct IsVector { static constexpr bool value = false; };
template <class T, class A>
struct IsVector<std::vector<T, A>> { static constexpr bool value = true; };
int main() {
std::cout << std::boolalpha
<< IsVector<int>::value << " "
<< IsVector<std::vector<int>>::value << " "
<< IsVector<std::vector<double, std::allocator<double>>>::value << " "
<< IsVector<const std::vector<int>>::value << "\n";
}
Example explained
Line 1The pattern names both of vector's parameters, so a custom allocator still matches.
Line 2`IsVector<std::vector<int>>` works because the default argument `std::allocator<int>` is filled in before matching, giving `A` something to bind to.
Line 3`const std::vector<int>` is a const-qualified type and matches only the primary, so the trait reports false; strip cv-qualifiers first if that is not what you want.
Line 4`value` is `constexpr`, so all four answers are computed while compiling and the program only prints them.
Important notes
Function templates cannot be partially specialised at all; overload them instead, and have the overload delegate to a partially specialised class template when the logic needs to branch on type structure.
A specialisation may introduce parameters the primary does not have, like `T` and `N` for `T[N]`, but each one must sit in a deducible position in the pattern or the specialisation is ill-formed.
Common mistakes
Writing `template <class T> struct S<T>` as the "partial" specialisation: the pattern is identical to the primary, so the compiler rejects it with a message about not specialising any template argument.
Creating two overlapping patterns where neither is more specialised, such as `S<T, T>` and `S<T*, U*>`; both definitions compile, and only the later `S<int*, int*>` fails with an ambiguity error, often far from the code that caused it.
Assuming top-level `const` or a reference is stripped before matching, so a trait with only a `std::vector<T, A>` pattern quietly answers false for `const std::vector<int>` instead of failing loudly.
Try it yourself
Change, predict, then run
Add `Describe<T&>` and `Describe<std::pair<A, B>>` specialisations to the main example, printing the pair as `pair of (first, second)`. Confirm that `Describe<std::pair<const int*, double[2]>>` prints `pair of (pointer to const value, array[2] of value)`.
Open the C++ workspaceCheck your understanding
With a primary `template <class T> struct S`, plus specialisations for the patterns `T*` and `T const`, which body does `S<int* const>` instantiate?
- The primary template, because a const pointer matches neither pattern
- `S<T*>` with `T = int`, since the top-level const is dropped before matching
- `S<T const>` with `T = int*`, and that body can then ask `S<int*>` about the rest
- Neither: the two patterns overlap here, so the instantiation is ambiguous
Show answer
`int* const` is a const-qualified pointer type, and the pattern `T const` matches it with `T = int*`. Option 2 is tempting because a function parameter of type `T` would happily discard the top-level const, but deduction against a specialisation pattern is an exact structural match: `T*` can only produce a cv-unqualified pointer type, so it never matches `int* const`. Since only one pattern matches, there is nothing to rank and nothing ambiguous.