C++ Part I · Module 18 · Templates and Generic Programming
Full and Partial Specialization
A template says one implementation serves every type, and sometimes that is false for one particular type. Chapter 18.2 already met a real instance: a function returning the smaller of two values compares two const char pointers as addresses rather than as text, which is almost never what the caller meant. That chapter's fix was an ordinary overload, and it worked. The language also provides a construct designed for exactly this, in two forms — one that replaces the template for a single exact type, and one that replaces it for a whole family such as any pointer, or any pair whose two halves happen to be the same type. For class templates, which is where most of the standard library's own specialisations live, both forms work cleanly and this chapter measures all four cases in one program. For function templates the picture is different and worth the measurement rather than the rule, because full specialisation of a function template interacts with overload resolution in a way that reliably surprises people: this chapter specialises a function for a pointer type, calls it with a pointer, and watches the call reach an entirely different function while the specialisation sits unused. Once you see why, the frozen advice that a plain overload is usually the better answer stops being a preference and becomes the obvious reading of the rules.
Advanced19 min readC++Part IFundamentalsTemplates
Part I · Module 18 · Chapter 18.8 · Full and Partial Specialization
1. The Need
A template claims one implementation fits every type. Sometimes it does not.
Chapter 18.2 §9's case was concrete: smaller(const T&, const T&) instantiated for const char* compares addresses, so smaller("pear","apple") returns whichever string literal the linker happened to place lower in memory. The algorithm is right and the operation it uses is wrong for that one type.
Three mechanisms address this, and the chapter's job is to say which to use when.
2. Full Specialization of a Class Template
Replace the template entirely, for one exact set of arguments:
template <typename T> // the PRIMARY template
struct Printer { static void print(const T&) { /* generic */ } };
template <> // FULL specialization for int
struct Printer<int> { static void print(const int& v) { std::printf("int: %d\n", v); } };A. class template specializations:
generic: <some T> <- Printer<double>, the primary
int: 42 <- Printer<int>, the full specializationtemplate <> with the arguments supplied in the name. The specialisation is a separate class — it need not have the same members as the primary, though a specialisation that omits members the primary has is a trap for anyone writing generic code against it.
3. Partial Specialization of a Class Template
Replace the template for a family of arguments, leaving some still parameterised:
template <typename T>
struct Printer<T*> { static void print(T* p) { /* any pointer */ } };
template <typename T>
struct Printer<Pair<T,T>> { static void print(const Pair<T,T>&) { /* same-type pair */ } }; pointer to something <- Printer<int*>, matched by the T* partial
a homogeneous pair <- Printer<Pair<int,int>>, matched by Pair<T,T>Both patterns matched. Printer<T*> covers every pointer type with one definition. Printer<Pair<T,T>> covers every pair whose halves are the same type — and crucially not Pair<int,double>, which falls through to the primary.
That second one shows what partial specialisation really is: pattern matching on the template arguments. You are writing a shape, and the compiler picks the most specialised shape that fits.
Non-type parameters can be matched too — Chapter 18.5's widths:
template <typename T> struct Buffer<T, 0> { /* the empty case */ };4. Which One Is Chosen
When several candidates match, the most specialised wins. For Printer<int*>:
| candidate | matches? |
|---|---|
Printer<T> (primary) | yes, with T = int* |
Printer<T*> (partial) | yes, with T = int — more specialised |
Printer<int> (full) | no |
The partial specialisation wins because every argument it matches is also matched by the primary, and not vice versa. A full specialisation, when it matches, always wins — it is maximally specialised.
And where several specialisations match equally, the compiler refuses rather than choosing. Measured:
template <typename T, typename U> struct Q { /* primary */ };
template <typename T> struct Q<T,int> { /* ... */ };
template <typename U> struct Q<int,U> { /* ... */ };
Q<int,int>::v();error: ambiguous partial specializations of 'Q<int, int>'
note: partial specialization matches [with T = int]
note: partial specialization matches [with U = int]An error naming both matches, not an arbitrary pick. Neither Q<T,int> nor Q<int,U> is more specialised than the other, so there is no most-specialised candidate.
And the non-ambiguous neighbours resolve exactly as expected:
Q<double,int>::v() = 1 (only <T,int> matches)
Q<int,double>::v() = 2 (only <int,U> matches)
Q<char,char>::v() = 0 (neither; the primary)A note on how I got this wrong first. My initial attempt at an ambiguous pair was Printer<T*> and Printer<const T> applied to const int*, which I expected to be ambiguous. It compiled cleanly, and the reason is worth knowing:
is_same<const int*, const int*> = 1
is_same<const int*, int* const> = 0 <-- different types!const int* is a pointer to a const int, not a const pointer. So Printer<const T> never matched it at all — only Printer<T*> did, with T = const int, and there was nothing to be ambiguous about. The rule was fine; my example was not, and the distinction between const int* and int* const is one Chapter 8.7 §3 establishes and which partial specialisation makes you care about precisely.
5. Where Specialisations Go
Chapter 18.7 §6's table:
A class template specialisation is still a template (fully specialised means no parameters left, but it is declared with template <>), so it goes in the header with the primary.
A full specialisation of a function template is an ordinary function, so it may go in a .cpp — and if it goes in a header it needs inline, because the ODR exemption of Chapter 18.7 §5 does not cover it.
That asymmetry is a consequence of §6, and it is the first hint that function specialisation is a different animal.
6. Function Templates: The Trap
A full specialisation of a function template looks like the obvious tool for Chapter 18.2 §9's problem. Measured:
template <typename T> void f(T) { /* "f<T>(T)" */ }
template <typename T> void f(T*) { /* "f<T>(T*) overload" */ }
template <> void f<int*>(int*){ /* "f<int*> specialization" */ }
int x = 0;
f(&x); f<T>(T*) overloadThe specialisation was not called. f(&x) reached f(T*) — a different primary template — and f<int*> sat unused.
The reason is a rule worth stating precisely, because it explains the whole chapter's advice:
Specialisations do not participate in overload resolution. Overload resolution chooses among the primary templates and ordinary functions first; only then are the chosen primary's specialisations consulted.
Here there are two primaries, f(T) and f(T*). Overload resolution preferred f(T*) for a pointer argument — correctly, it is a better match. The specialisation f<int*> was written against f(T), the primary that lost. So it was never in the running.
Two consequences that make this genuinely dangerous.
It compiles silently. No warning says the specialisation is unreachable.
And it is order-sensitive in a way that looks arbitrary. Declare f(T*) after the specialisation and the specialisation still loses; declare it before and nothing changes. What matters is which primary the specialisation names, and that is decided by which primaries were visible when the specialisation was declared.
7. The Overload, Which Works
Chapter 18.2 §9 measured the alternative:
template <typename T> const T& smaller(const T& a, const T& b) { return b < a ? b : a; }
const char* smaller(const char* a, const char* b) { return std::strcmp(b,a) < 0 ? b : a; }A. exact match for BOTH (const char*): plain : apple
B. only the template matches (int): template : 3
C. forcing the template with <>: template : pear (compared the POINTERS)The plain function won on an equal match, which is Chapter 6.7's resolution plus one rule: a non-template is preferred over a template specialisation when neither needs a worse conversion.
And an overload does participate in overload resolution — it is an ordinary function, considered alongside the primaries, chosen by the normal rules. There is no second phase in which it can be bypassed.
8. So: Which Mechanism
| Situation | Use |
|---|---|
| a class template, one exact type | full specialisation |
| a class template, a family of types | partial specialisation |
| a function template, one type | an ordinary overload |
| a function template, a family | an overload with a more specialised signature |
| behaviour varying on a compile-time property | if constexpr — §9 |
The rule in one line:
Specialise class templates. Overload function templates.
Partial specialisation of a function template does not exist as a language feature — what looks like it is a second primary template, which is exactly §6's trap. That alone settles the function case.
And a specialisation that drifts from the primary's interface breaks generic code, which is worth measuring because it is the failure mode of §2's "the specialisation is a separate class":
template <typename T> struct Box { T v; T get() const { return v; } };
template <> struct Box<bool> { bool v; bool value() const { return v; } }; // renamed!
template <typename T> T readIt(const Box<T>& b) { return b.get(); } // generic codeerror: no member named 'get' in 'Box<bool>'
note: in instantiation of function template specialization 'readIt<bool>' requested herereadIt works for every type except the one that was specialised. Nothing warned when Box<bool> was declared with a different interface; the failure waited until generic code tried to use it, and arrived through Chapter 18.6 §5's instantiation note.
The standard library ships an instance of exactly this, and it is worth measuring because it is the most-cited cautionary specialisation in C++. std::vector<bool> is a full specialisation that stores bits, so its operator[] cannot return a bool& — there is no addressable bool to refer to:
A. what does operator[] return?
vector<int>::reference is int& ? 1
vector<bool>::reference is bool& ? 0 <-- a PROXY, not a referenceThe consequence that bites:
B. so `auto` behaves differently:
decltype(vi[0]) via auto is int ? 1
decltype(vb[0]) via auto is bool ? 0 <-- it is the proxy
C. and the proxy keeps writing to the vector:
after `auto rb = vb[0]; rb = false;` vb[0] = 0
after `auto ri = vi[0]; ri = 0;` vi[0] = 1auto rb = vb[0]; rb = false; modified the vector. The int version did not — vi[0] is still 1. Identical-looking code, opposite behaviour, decided by which specialisation operator[] came from. And an address cannot be taken at all:
bool* p = &vb[0];
error: no viable conversion from '__bit_iterator<std::vector<bool, …>, false…So std::vector<bool> is Box<bool> above, shipped in the standard: generic code written against std::vector<T> and tested on int can behave differently or fail to compile on bool. Module 19 covers the consequences for using it; the lesson here is §2's — a specialisation is a separate class, and nothing obliges it to keep the primary's interface.
9. The Modern Alternative
For behaviour that varies on a compile-time property rather than on an exact type, C++17 offers something cleaner than any specialisation:
template <typename T>
void describe(const T& v)
{
if constexpr (std::is_pointer_v<T>) { /* the pointer branch */ }
else if constexpr (std::is_integral_v<T>) { /* the integral branch */ }
else { /* the general branch */ }
}if constexpr discards the untaken branches at compile time, so each branch need only be valid for the types that reach it. Measured — note that the pointer branch dereferences and the integral branch casts, operations that are illegal for the other types:
pointer to 7
integral 42
something elseAll three compiled, with 0 warnings, because each branch was discarded for the types it would not have suited. Replace if constexpr with a plain if and the untaken branches are compiled after all:
error: cast from pointer to smaller type 'int' loses informationThat is Chapter 18.6 §3's dependent-construct rule being used deliberately: if constexpr makes the discarding explicit instead of relying on separate specialisations to keep incompatible code apart. One function, no specialisations, no §6 trap, and the branching visible in one place.
Chapter 18.10 §7 is where this belongs as a design technique. For this chapter the point is narrower: much of what full specialisation of a function template was used for before C++17 is now better written as if constexpr.
10. What to Actually Do
Specialise class templates — fully for one type, partially for a family.
Overload function templates — never fully specialise one.
Remember partial specialisation of a function template does not exist.
Put class template specialisations in the header; a function specialisation is an ordinary function and needs inline if it goes in one.
Expect the most specialised match to win, and expect ambiguity to be an error.
Use if constexpr when the variation is on a property rather than an exact type.
And keep a specialisation's interface compatible with the primary's — std::vector<bool> is the warning.
11. Mistakes Worth Making Once
Fully specialising a function template. Measured: f<int*> was never called — a different primary won resolution.
Expecting a specialisation to participate in overload resolution. It does not; the primary is chosen first.
Expecting a warning for an unreachable specialisation. There is none.
Trying to partially specialise a function template. The feature does not exist; what you write is a second primary.
Putting a class template specialisation in a .cpp. It is a template; it belongs in the header.
Putting a function specialisation in a header without inline. It is an ordinary function.
Writing two partial specialisations where neither is more specialised. An ambiguity error.
Giving a specialisation a different interface from the primary. Generic code against the primary breaks — std::vector<bool>.
Reaching for specialisation when if constexpr would do.
12. Interview Relevance
"What is the difference between full and partial specialisation?" Full supplies all the arguments; partial leaves some parameterised and matches a family. The strong answer describes partial specialisation as pattern matching on template arguments.
"Can you partially specialise a function template?" No — the feature does not exist. What looks like it is an overload.
"What is wrong with fully specialising a function template?" Specialisations do not participate in overload resolution — the primary is chosen first. Measured: a specialisation for int* was bypassed because a different primary won. This is the discriminator.
"So what do you do instead?" An ordinary overload, which participates in resolution normally and is preferred on an equal match.
"Which specialisation wins when several match?" The most specialised; ambiguity is an error.
"Where do specialisations go?" Class specialisations in the header; a function specialisation is an ordinary function and needs inline in a header.
"What would you use instead in modern C++?" if constexpr, when the variation is on a compile-time property rather than an exact type.
13. What You Should Be Able to Say Now
- Specialisation provides a different implementation for a type or a family of types.
- Full specialisation supplies every argument —
template <> struct Printer<int>. - Measured:
Printer<double>reached the primary andPrinter<int>the full specialisation. - Partial specialisation leaves some arguments parameterised and is pattern matching on template arguments.
- Measured:
Printer<T*>matchedint*, andPrinter<Pair<T,T>>matchedPair<int,int>— but notPair<int,double>. - Non-type arguments can be matched too — Chapter 18.5's widths.
- The most specialised match wins, and ambiguity is an error rather than an arbitrary choice.
- Measured, the trap:
f<int*>was specialised,f(&x)was called, andf(T*)ran instead — the specialisation was unused. - Specialisations do not participate in overload resolution — a primary is chosen first, then its specialisations.
- It compiles with no warning, and which primary the specialisation names decides everything.
- Partial specialisation of a function template does not exist.
- Measured: an ordinary overload won on an equal match and behaved correctly for
const char*. - So: specialise class templates, overload function templates.
- Class specialisations go in the header; a function specialisation is an ordinary function and needs
inlinethere. if constexpris the C++17 answer when the variation is on a property, not a type.- Measured: two equally-matching partial specialisations gave
ambiguous partial specializations of 'Q<int, int>'with a note for each match. - My own first ambiguity example was not ambiguous, because
const int*is a pointer toconst intrather than aconstpointer — onlyP<T*>matched it. - Measured:
if constexprcompiled all three branches of a function whose pointer branch dereferenced and whose integral branch cast — 0 warnings; a plainifgavecast from pointer to smaller type 'int' loses information. - Measured: a specialisation that renamed a member broke generic code —
no member named 'get' in 'Box<bool>', reported through an instantiation note. - Measured:
std::vector<bool>::referenceis notbool&,auto rb = vb[0]; rb = false;wrote through to the vector where theintversion did not, and&vb[0]does not compile. - A specialisation is a separate class and may differ from the primary's interface — which is what makes
std::vector<bool>the standing warning.
14. Exercises
Exercise 1 — full and partial. For a Formatter<T>, write the primary, a full specialisation for bool, and a partial one for any std::vector<T>.
Exercise 2 — pattern matching. Say which of Formatter<int>, Formatter<int*>, Formatter<std::vector<int>> and Formatter<std::vector<std::vector<int>>> each of your definitions matches.
Exercise 3 — the ordering. For Printer<T>, Printer<T*> and Printer<int*>, say which is chosen for int* and why.
Exercise 4 — reproduce the trap. Write the three f declarations, call with a pointer, and say which runs and why the specialisation does not.
Exercise 5 — state the rule. Explain in one sentence why a function template specialisation can be unreachable.
Exercise 6 — fix it. Rewrite the trap using an overload and say what changes about resolution.
Exercise 7 — placement. Say where each goes: a class full specialisation, a class partial specialisation, a function full specialisation, a function overload.
Exercise 8 — modernise. Rewrite a two-way function specialisation as one function using if constexpr.
15. Summary
- Specialisation gives one type, or a family, a different implementation.
- Measured: full (
Printer<int>) and partial (Printer<T*>,Printer<Pair<T,T>>) specialisations all matched as intended, with the primary catching the rest. - Partial specialisation is pattern matching on template arguments, and the most specialised match wins; ambiguity is an error.
- Measured, the trap: a full specialisation
f<int*>was never called —f(&x)reached a different primary, silently. - Because specialisations do not participate in overload resolution — the primary is chosen first.
- Partial specialisation of a function template does not exist.
- Measured: an ordinary overload won on an equal match and did the right thing.
- So specialise class templates and overload function templates.
if constexprreplaces most function specialisation in C++17, andstd::vector<bool>is the warning about interfaces that drift from the primary.
One mechanism remains before the module's closing argument, and it is the one that makes several facilities used throughout this curriculum possible. std::make_unique takes whatever arguments the constructed type's constructor takes, however many there are and whatever their types; emplace_back does the same; a type-safe logging call takes a format and any number of values. None of that is expressible with the templates built so far, because every one of them has a fixed number of parameters. The next chapter is the mechanism that removes that restriction — a parameter that stands for any number of arguments — along with the C++17 addition that makes using one a single line rather than a recursive pair of functions, and the measurement of the forwarding idiom that Chapter 14.4 relied on without explaining.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
