C++ / TEMPLATES AND GENERIC PROGRAMMING
Template specialisation for exceptional cases
Use template<> to replace a class template's body for one exact type, specialise a single member, and know why function specialisations get bypassed.
What you will learn
- Write template<> class specialisations for types the generic body cannot handle
- Specialise one member of a class template and leave the other members generic
- Predict which function runs when a specialisation competes with an overload
- Place specialisations before first use, and mark specialised functions inline in headers
Understanding Template specialisation for exceptional cases
The primary template is a recipe the compiler follows for whatever arguments you hand it. An explicit specialisation, written as template <> followed by the fully spelled-out argument list, is not another recipe but a finished result the compiler hands back whenever those exact arguments appear. Because the match has to be exact, there is no ranking to reason about: either the argument is bool and you get the specialisation, or it is not and the primary is instantiated as usual.
For class templates a specialisation replaces the whole definition, not just the parts you disagree with. Formatter<bool> below shares nothing with Formatter<T>: no members, no base classes, not even the same data layout, and any helper the primary offered has to be written again by hand. That total independence is exactly the point, because the compiler never instantiates the primary's body for the specialised argument at all, which is why this technique rescues types on which the generic code would be a hard compile error rather than merely wrong.
Functions behave differently, and this is where the feature earns its bad reputation. Overload resolution only ranks primary templates and ordinary functions; a specialisation is looked up afterwards, once the winning template is already known, so a specialisation of f(T) is quietly ignored when a separate f(T*) overload wins the call. One ordering rule applies to classes and functions alike: the specialisation must be visible before the first use that would instantiate those arguments, otherwise the program is ill-formed and two translation units can disagree about what your code does.
<iostream>
<string>
// primary template: the fallback used for every type
template <typename T>
struct Formatter {
static std::string apply(const T& value) { return std::to_string(value); }
};
// exceptional case: to_string(bool) would produce "1"
template <>
struct Formatter<bool> {
static std::string apply(bool value) { return value ? "true" : "false"; }
};
// exceptional case: to_string(std::string) does not compile at all
template <>
struct Formatter<std::string> {
static std::string apply(const std::string& value) { return '"' + value + '"'; }
};
int main() {
std::cout << Formatter<int>::apply(42) << '\n';
std::cout << Formatter<double>::apply(0.5) << '\n';
std::cout << Formatter<bool>::apply(true) << '\n';
std::cout << Formatter<std::string>::apply("hi") << '\n';
}
An explicit specialisation is a replacement definition for one exact set of template arguments, not a new template that competes for the call.
Worked examples
A function specialisation that never runs
Shows that overload resolution picks a primary template first, so a specialisation attached to the losing template is skipped.
<iostream>
template <typename T> void f(T) { std::cout << "f(T)\n"; }
// specialises f(T), the only f visible at this point
template <> void f<int*>(int*) { std::cout << "f<int*>(int*)\n"; }
// a second, more specialised primary template
template <typename T> void f(T*) { std::cout << "f(T*)\n"; }
int main() {
int x = 0;
int* p = &x;
f(x);
f(p);
f<int*>(p);
}
Example explained
Line 1template <> void f<int*>(int*) binds to f(T), because f(T*) has not been declared yet when that line is read.
Line 2f(p) compares only the two primary templates; partial ordering prefers f(T*) for a pointer, so the specialisation is never even considered.
Line 3f<int*>(p) fixes T to int*, which turns f(T*) into f(int**) and makes it non-viable, so f(T) wins and its specialisation finally runs.
Line 4Moving the f(T*) declaration above the template <> line would change which template gets specialised, and therefore change the output of f(p).
Specialising a single member
Replaces only the member that misbehaves for const char*, while the rest of the class is still generated from the primary template.
<cstring>
<iostream>
template <typename T>
struct Box {
T value;
void describe() const { std::cout << "value " << value << '\n'; }
void twice() const { describe(); describe(); }
};
// only describe() is specialised; twice() stays generic
template <>
void Box<const char*>::describe() const {
std::cout << "text of length " << std::strlen(value) << '\n';
}
int main() {
Box<int>{7}.twice();
Box<const char*>{"hello"}.twice();
}
Example explained
Line 1template <> void Box<const char*>::describe() const specialises one member function of an implicitly instantiated class, so no second class body is needed.
Line 2twice() is never specialised, yet inside Box<const char*> it calls the specialised describe(), because the call is bound when that instantiation is generated.
Line 3Box<int> is untouched and still streams the int through the primary definition.
Line 4The specialised member must appear before main; if it came after, describe() would already have been instantiated from the primary and the program would be ill-formed.
Important notes
An explicit specialisation of a function or member function is not implicitly inline, so defining one in a header without inline gives duplicate symbol errors at link time; a specialised class definition in a header is fine.
You may add a template <> specialisation of a standard template such as std::hash for your own type, but specialising it for a standard type, or adding a plain overload inside namespace std, is not allowed.
Common mistakes
Putting the specialisation after code that already used the instantiation, or in only one .cpp instead of the shared header: the compiler reports specialization after instantiation, or worse, links successfully with generic behaviour in one object file and specialised behaviour in another.
Assuming Formatter<bool> keeps the primary template's members: the specialisation is an unrelated class, so a helper you only wrote in the primary is simply absent and the call fails with no member named ... errors.
Adding template <> to a function hoping to override an existing overload: overload resolution never sees the specialisation, so the overload keeps winning and the specialised body is dead code that still compiles.
Try it yourself
Change, predict, then run
Add a template <> struct Formatter<char> that returns the character wrapped in single quotes and check that Formatter<int> still goes through std::to_string. Then move that specialisation below main and read the exact error the compiler gives you.
Open the C++ workspaceCheck your understanding
Given the declarations, in this order: template <typename T> void g(T); template <> void g<double*>(double*); template <typename T> void g(T*); which definition runs for double d; g(&d);?
- g<double*>(double*), because an explicit specialisation is the most exact match and always wins
- g(T*) with T = double, because only primary templates take part in overload resolution and g(T*) is the more specialised of the two
- The call is ambiguous, since g(T) and g(T*) both accept a double* argument equally well
- g(T) with T = double*, because it was declared first and declaration order breaks the tie
Show answer
Overload resolution builds its candidate set from the two primary templates only, and partial ordering prefers g(T*) for a pointer argument, so g(T*) is called. The first option is tempting because the specialisation names the argument type exactly, but a specialisation is never a candidate: it can only replace the body of the template it specialises, and here that template, g(T), lost the call. Declaration order matters only for deciding which template the template <> line specialises, not for tie-breaking.