A class template is a recipe for a type, the way a function template is a recipe for a function:
template<class T>
struct Pair {
T first;
T second;
T larger() const {
return first > second ? first : second;
}
};
Pair is not a type. Pair<int> and Pair<double> are — and they are unrelated types, no more connected than int and std::string.
How They Differ From Function Templates
Same core idea — a pattern the compiler stamps out per set of arguments. The differences:
| Function template | Class template | |
|---|---|---|
| Deduction | Always, from the call arguments | Never, pre-C++17. CTAD deduces from constructor args, but it’s a separate, weaker algorithm |
| Explicit args | Usually optional | Mandatory, except under CTAD |
| Partial specialization | Illegal | Legal, and the main tool |
| Overloading | Yes, and it’s how you get specialization-like behaviour | No such thing. Class names don’t overload |
| Selection | Overload resolution at the call site, competing with every visible f | Matched against specializations at instantiation, in isolation |
| Instantiation granularity | Whole function or nothing | Lazy per member. Unused members are never instantiated |
| Member templates | n/a | Members can themselves be templates, independently parameterized |
Full Specialisation
Sometimes, the general pattern produces the wrong code for a specific type, so we have to override it like so:
template<class T>
struct Printer {
void print(T x) { std::cout << x; }
};
template<>
struct Printer<bool> {
void print(bool x) { std::cout << (x ? "true" : "false"); }
};
It’s called full specialisation because you’ve specialised the parameters fully: none are left free.
Partial Specialisation
template<class T, class U> struct P { }; // 2 free -> primary
template<class T> struct P<T, int> { }; // 1 free -> partial
template<> struct P<char, int> { }; // 0 free -> full
template<class T, class U>
struct Pair {
T first;
U second;
};
// PARTIAL: both the same type, so larger() now makes sense
template<class T>
struct Pair<T, T> {
T first;
T second;
T larger() const { return first > second ? first : second; }
};
// FULL: exactly const char*, where > compares pointers, not contents
template<>
struct Pair<const char*, const char*> {
const char* first;
const char* second;
const char* larger() const {
return std::strcmp(first, second) > 0 ? first : second;
}
};
Why You Never Specialise a Function Template
Both files declare the same three functions and make the same call. The specialization sits on line 2 in the first and line 3 in the second.
// File A
template<class T> void f(T) { puts("1"); }
template<> void f(int*) { puts("2"); } // <-- specialization here
template<class T> void f(T*) { puts("3"); }
f(p); // prints 3
// File B
template<class T> void f(T) { puts("1"); }
template<class T> void f(T*) { puts("3"); }
template<> void f(int*) { puts("2"); } // <-- specialization here
f(p); // prints 2
A specialization has to attach to one base template, and the compiler decides which at the moment it reads the line, using only what has been declared above it.
In file A, only f(T) exists yet, so the specialization attaches there. Then f(T*) shows up later and wins the call — and it has no specialization of its own, so its plain body runs.
In file B, f(T*) is already visible, so the specialization attaches to it instead. f(T*) still wins the call, but this time it does have a specialization.
The reason this matters beyond trivia is that those declarations normally live in headers:
#include "core.h" // declares f(T) and specializes f(int*)
#include "pointers.h" // adds the f(T*) overload
Swap those two #include lines and your program’s behaviour changes, silently, with no warning from any compiler. Or someone adds pointers.h to a precompiled header six months from now, and something starts misbehaving in a translation unit nobody touched.
Which is why the advice is absolute: overload function templates, never specialize them. Overloads are all base templates, so they all participate in the same resolution and there is no attachment step to get wrong.
Instantiation Is Per Translation Unit
Expand
Each .cpp that mentions Pair<int> gets its own copy instantiated — TUs compile in isolation. Both objects emit the same symbol, and the linker discards the duplicates:
a.o: weak Pair<int>::larger() const
b.o: weak Pair<int>::larger() const
-> linker keeps one
The same weak-symbol deduplication as Pt 1. Only the members reach the object file: Pair<int> itself is compile-time only and produces no symbol at all.
Appendix: Specialisation Reference
Expand
Class templates
| What | Code |
|---|---|
| Primary | template<class T, class U> struct P { }; |
| Full specialization | template<> struct P<int, char> { }; |
| Partial: pin one param | template<class U> struct P<int, U> { }; |
| Partial: match a pattern | template<class T, class U> struct P<T*, U> { }; |
T&, const T, T,T and vector<T> are the last row again with different decoration.
Function templates
| What | Code | Legal |
|---|---|---|
| Primary | template<class T, class U> void f(T, U); | yes |
| Full specialization | template<> void f(int, char); | yes, avoid |
| Partial specialization | template<class T> void f<T, T>(T, T); | no |
| Overload instead | template<class T> void f(T, T); | yes, do this |
The illegal row is not a syntax quibble — the compiler says so directly:
error: function template partial specialization is not allowed
Specialization vs instantiation
| Code | Meaning |
|---|---|
template<> struct P<int, char> { }; | specialization: replace the pattern |
template struct P<int, char>; | instantiation: emit the pattern here |
extern template struct P<int, char>; | don’t emit, someone else did |
Variable templates
template<class T> constexpr bool is_ptr = false;
template<class T> constexpr bool is_ptr<T*> = true;
This is the shape of essentially every type trait in the standard library.
Check: replaces, not extends
template<class T> struct Box { int a; };
template<class T> struct Box<T*> { int b; };
Box<int*> x;
Does x have a, b, or both?
Answer
b only. sizeof(x) is 4, and touching a fails:
error: no member named 'a' in 'Box<int *>'
A partial specialization replaces the primary template for matching arguments — it does not extend it. Box<T*> is a separate definition that happens to be selected for pointer types; nothing from the primary’s body carries over. Inheriting the common part is a deliberate act:
template<class T> struct Box<T*> : Box<void> { int b; };
TODO
Expand
All snippets below are verified against clang 21 — paste and expand.
Partial specialisation section
1. The type-trait payoff. The section shows the mechanism but never why anyone reaches for it. This is the reason, and it pairs with the is_ptr variable template already in the appendix:
template<class T> struct remove_pointer { using type = T; };
template<class T> struct remove_pointer<T*> { using type = T; };
2. Most specialised wins. The appendix has full > partial > primary, but not how two competing partials rank:
template<class T> struct S<T*> { /* 1 */ };
template<class T> struct S<const T*> { /* 2 */ };
S<const int*> // -> 2, const T* is more specialised
It can also fail outright: P<T,int> and P<int,U> both match P<int,int>, giving error: ambiguous partial specializations.
3. What you can match on. The appendix claims T&, const T and vector<T> are the same idea as T*, but the body only ever shows T,T:
template<class T> struct S<T*> { }; // any pointer
template<class T> struct S<const T> { }; // any const
template<class T> struct S<T&> { }; // any lvalue ref
template<class T> struct S<std::vector<T>> { }; // any vector
template<class T, int N> struct Arr<T, 0> { }; // non-type parameter
4. Two legality rules. The pattern must be narrower than the primary, and every parameter must be deducible from it:
template<class T> struct S<T> {}; // does not specialize any template argument
template<class T> struct S<int> {}; // does not use any of its template parameters
5. Motivation. Full Specialisation opens with a reason (“produces the wrong code”); Partial Specialisation opens cold into code. The natural hook: one specialisation covers a whole family, instead of hand-writing Pair<int*>, Pair<char*>, Pair<Foo*>.
Missing topics
if constexprand concepts. “How would you do this today?” — often you wouldn’t specialise at all. Worth a short section on when specialisation is still the right tool.- Declare before use. Specialising after the type’s first use is an error (
explicit specialization of 'S<int>' after instantiation), or worse, an ODR violation when one TU sees the specialisation and another doesn’t. - Dependent names. Cut from the comparison table, so
typename T::x,this->base_memberand two-phase lookup now appear nowhere.
Editorial
- “override” in Full Specialisation collides with the
overridekeyword from the Virtual Dispatch post. “replace” or “specialise” sits closer to the series. - Two different
Pairtemplates. The intro takes one parameter, the partial-spec example takes two; they cannot coexist in one TU (too many template parameters in template redeclaration). Left deliberately — each block reads standalone — but the per-TU aside still refers to the intro’sPair<int>::larger(). - Voice pass on the intro paragraph under the first
Pairblock and the per-TU collapsible. descriptionin the frontmatter is still-.