Intuition What this page is
The parent note told you the rules . Here we drill every shape a specialization problem can take — every match, every miss, every tie — so no exam or codebase can surprise you. Think of it as walking every square of a chessboard so you know what's on each one.
This page assumes you have met the basic template syntax . If template<class T> still looks alien, read that first.
Before any example, let us nail down two words we use constantly.
Definition Pattern and shape gate
Pattern — the type expression written in a specialization's angle brackets, e.g. the T* in struct IsPtr<T*>. It is a stencil : the compiler tries to lay it over your actual type X and asks "can I fill in the placeholders (T, A, N, …) so the stencil becomes exactly X?" If yes, the pattern matches X.
Shape gate — an informal name for that yes/no test. A pattern like T* is a gate that only opens for pointer types, because only a pointer can be written as "some T, followed by a *". int cannot pass through the T* gate; int* can (with T = int).
So "matching" = "does the pattern's stencil fit X?" and "shape gate" = the same idea, pictured as a doorway that only certain type-shapes fit through.
The figure below draws the doorway metaphor literally — keep it in mind, because every example on this page is just "which gates does X walk through?"
Look at the three doorways. Each is cut into the shape of a pattern : the widest opening is T (the primary — any type fits), the medium one is T* (only pointer-shaped types fit), and the narrowest is int* (only that exact type fits). Watch the two travellers: the yellow int block is not pointer-shaped, so it can only walk through the widest T door. The pink int* block is pointer-shaped, so it fits through all three doors — and the compiler always sends it through the narrowest door it fits, which is int*. That "narrowest door wins" is the whole selection rule in one picture.
Keep the two words — pattern and shape gate — in mind: every example below is just "which patterns' gates does X pass through, and which survivor is most specialized?"
Every specialization question is really "which candidate wins for type X? " The candidates are: the primary (the generic fallback), a full spec (all parameters pinned to concrete types), and one or more partial specs (shape constrained, some parameters still free). The type X you feed in is the input ; the winning candidate is the output .
The map below lays out every case class as a landscape of shape gates rather than a wall of text. Read it as a decision fan: an input X enters at the left and lands in exactly one cell depending on how many gates it fits and how those gates relate.
Each labelled node in the figure is a cell of the matrix — a distinct scenario the topic can throw at you. The colour tells you the outcome (white = primary fires, blue = a partial fires, yellow = a full spec fires, pink = an error). The tiny tag under each node (Ex 1, Ex 2, …) points to the worked example that drills it. The cells are:
A — no spec matches → the primary fires (the fallback). (Ex 1)
B — a full spec matches → it beats everything, because X is exactly the pinned type. (Ex 1, Ex 8)
C — a partial matches and the primary also matches → the partial wins. (Ex 2)
D — one parameter pinned , another still free (mixed concrete + free). (Ex 3)
E — two candidates compete , one is strictly more specialized → it wins by subset ordering. (Ex 4)
F — two partials tie , neither a subset of the other → ambiguity error (the degenerate case). (Ex 5)
G — nested shape (T**, pointer-to-pointer) → the most-nested pattern wins. (Ex 6)
H — zero / degenerate input such as void and void* (a boundary type). (Ex 7)
I — a real-world word problem : a per-type serializer. (Ex 8)
J — the exam twist : trying to partially specialize a function → use overloading instead. (Ex 9)
K — limiting behaviour : reference vs pointer vs array shapes are all distinct, each firing its own partial. (Ex 10)
Every worked example below is tagged with the cell(s) it covers. Together they fill the whole map.
Read the figure top to bottom: a type X drops in (top, pale-yellow box), the compiler collects all candidates (white box), throws out the ones whose pattern fails the shape gate (pink box — "discard those whose pattern does not match X"), then among survivors picks the most specialized (blue box). The three outcome boxes at the bottom — primary / partial / full — are the three colours you'll see repeated in every example: white for the fallback, blue for a partial, yellow for a full spec.
Worked example Example 1 — Cell A + B: primary vs full, the two extremes
template < class T > struct Printer { static const char* fmt (){ return "value" ; } };
template <> struct Printer < bool > { static const char* fmt (){ return "true/false" ; } };
Printer < double >:: fmt (); // ?
Printer < bool >:: fmt (); // ?
Forecast: Guess both outputs before reading on. Which type has a custom recipe?
List the candidates for double. Only the primary; the full spec is pinned to bool, and double ≠ bool.
Why this step? Listing-then-filtering is always the first move: matching (the shape gate) is the first gate, so a candidate that doesn't match X is discarded before any "specificity" contest even begins. We must know who is still standing before we can compare them.
No spec matches double → primary fires. So Printer<double>::fmt() returns "value". (Cell A.)
Why this step? When only the fallback survives, there is nothing to compare it against — the winner is decided by default.
List the candidates for bool. Primary matches (any T) and full spec Printer<bool> matches exactly.
Why this step? Same reason as step 1 — we re-run the filter for the new input, because a different X can pass a different set of gates.
Full beats primary. A full spec is the most specific thing possible — zero free parameters. So Printer<bool>::fmt() returns "true/false". (Cell B.)
Why this step? "You named the exact type" is the strongest possible signal of intent.
Verify: Two distinct types, two distinct outputs — the caller uses one name Printer and gets type-appropriate behaviour. "value" for double, "true/false" for bool. ✓
Worked example Example 2 — Cell C: partial
T* beats primary
template < class T > struct IsPtr { static const bool value = false ; };
template < class T > struct IsPtr < T * > { static const bool value = true ; };
IsPtr < int >::value; // ?
IsPtr < int* >::value; // ?
Forecast: Which one comes out true?
Feed int. Primary matches (T = int). Does IsPtr<T*> match? It requires X to be a pointer ; int is not. Discard the partial.
Why this step? We always start by running each pattern's shape gate against X. The pattern T* is a gate that only opens for pointer types, so int bounces off it.
Only primary survives → false. So IsPtr<int>::value == false. (Cell A again, for contrast.)
Feed int*. Primary matches (T = int*). Partial T* also matches with T = int. Two survivors.
Partial ordering. Everything matching T* (pointers) is a strict subset of everything matching T (all types). So T* is more specialized → it wins.
Why this step? Subset = "you clearly meant the narrower case."
Result: IsPtr<int*>::value == true. (Cell C.)
The figure below draws this exact subset relationship — pause on it, because every "partial beats primary" decision on this page is the same picture.
Look at the two circles: the big white circle is "all types that match T" (the primary). The inner blue circle is "pointer types that match T*" (the partial). The pink dot int* sits inside both circles, so both patterns match — but the blue circle is entirely inside the white one, which is exactly what "T* is more specialized than T" means. The yellow dot int sits only in the white circle, so only the primary fires for it.
Verify: This is literally how std::is_pointer is built — is_pointer<int>::value is false, is_pointer<int*>::value is true. Our miniature reproduces the STL trait exactly. ✓
Worked example Example 3 — Cell D: pin ONE parameter, leave the other free
template < class A , class B > struct Pair2 { static const char* k (){ return "AB" ; } };
template < class B > struct Pair2 < int , B > { static const char* k (){ return "intB" ; } };
Pair2 < double , char >:: k (); // ?
Pair2 < int , char >:: k (); // ?
Pair2 < int , int >:: k (); // ?
Forecast: Three calls — predict all three.
Pair2<double,char>: run the gate. The partial's pattern is <int, B>, which demands the first argument be exactly int; here it's double, so the gate stays shut. Discard partial → primary → "AB". (Cell A.)
Why this step? As always, filter before ranking — a pattern that fails to match cannot even enter the specificity contest.
Pair2<int,char>: partial matches with B = char, primary also matches. Partial pins one slot (a subset) → more specialized → wins → "intB". (Cell D.)
Why this step? Pinning even one parameter to a concrete type narrows the family, and narrower wins.
Pair2<int,int>: here A=int and B=int. The partial still only pins the first slot; the second slot B is free and happily binds to int. Only one partial exists, so no competition → "intB". (Cell D.)
Why this step? A free parameter binding to int is not the same as a second pinned parameter — there is only one partial here, so no ambiguity.
Verify: Outputs are "AB", "intB", "intB". The first slot being int is the trigger; the second slot's value is irrelevant to which candidate fires. ✓
Worked example Example 4 — Cell E: two candidates, one strictly more specialized
template < class T > struct S { static const char* w (){ return "generic" ; } };
template < class T > struct S < T * > { static const char* w (){ return "ptr" ; } };
template <> struct S < int* > { static const char* w (){ return "int-ptr" ; } };
S < double* >:: w (); // ?
S < int* >:: w (); // ?
Forecast: For int*, three candidates match. Who wins?
S<double*>: run the gate on each candidate. Primary matches; S<T*> matches with T=double; the full S<int*> does not (double ≠ int). Two survivors: primary and T*. T* is more specialized → "ptr". (Cell C.)
Why this step? We filter first so we know the full spec is out of the running for double* — otherwise we might wrongly expect "int-ptr".
S<int*>: all three match! Primary (any T), partial T* (with T=int), full S<int*> (exact).
Why this step? Re-running the gate for the new input reveals that this time three candidates survive, so the tie-break ladder actually matters here.
Full beats partial beats primary. The full spec has zero free parameters — maximally specific → it wins → "int-ptr". (Cell E, resolved by ordering.)
Why this step? This shows the full ordering ladder: full ⟵ more constrained partial ⟵ less constrained partial ⟵ primary.
Verify: "ptr" for double*, "int-ptr" for int*. The full spec cleanly overrides even a matching partial. ✓
Worked example Example 5 — Cell F (degenerate): two partials TIE → error
template < class A , class B > struct M { };
template < class A > struct M< A , int > { }; // matches <X, int >
template < class B > struct M< int , B > { }; // matches <int, X >
M <int , int> m; // ?
Forecast: Does this compile?
Candidates for M<int,int>: run each gate. Primary matches; M<A,int> matches (A=int); M<int,B> matches (B=int). Two partials survive plus primary.
Why this step? We must enumerate the survivors before checking ordering, because ambiguity is a property of the surviving set , not of any single candidate.
Compare the two partials. Is "second arg is int" a subset of "first arg is int"? No — M<char,int> matches the first but not the second. Is the reverse a subset? Also no — M<int,char> matches the second but not the first.
Why this step? Subset in neither direction = neither is more specialized.
Rule 5 fires: ambiguity error. The compiler refuses to guess. (Cell F — the degenerate case.)
Why this step? Guessing silently would be a footgun; a hard error forces you to disambiguate.
Verify: This does not compile — the assertion here is "compilation fails with an ambiguity diagnostic," not a runtime value. The fix: add a third full spec template<> struct M<int,int>{}; which beats both partials. ✓ (No numeric check — the outcome is a compile error.)
Worked example Example 6 — Cell G: nested shapes, most-nested wins
template < class T > struct N { static int id (){ return 0 ; } }; // any T
template < class T > struct N < T * > { static int id (){ return 1 ; } }; // any pointer
template < class T > struct N < T ** > { static int id (){ return 2 ; } }; // pointer-to-pointer
N < int >:: id (); // ?
N < int* >:: id (); // ?
N < int** >:: id (); // ?
Forecast: Rank the three inputs by which id they produce.
int: run the gates — neither T* nor T** opens for a non-pointer, so only primary survives → 0.
Why this step? Filtering first tells us immediately that both pointer patterns are irrelevant for a plain scalar, so no ranking is needed — the fallback wins by default.
int*: primary matches (T = int*) and T* matches (T = int). Does T** match? That needs a pointer-to-pointer ; int* is a pointer-to-int, not pointer-to-pointer, so the T** gate stays shut. Discard T**. Between primary and T*, the partial T* is the strict subset → it wins → 1.
Why this step? We must check the deepest gate too and confirm it fails, otherwise we might wrongly expect 2; only then can we rank the survivors.
int**: all three match. Primary (T = int**); T* matches because int** is a pointer (to int*), so T = int*; and T** matches with T = int. Three survivors.
Why this step? Re-running every gate for the deepest input shows the full stack of candidates before we rank them.
T** is a strict subset of T* , which is a strict subset of T (every pointer-to-pointer is a pointer, and every pointer is a type, never the reverse). So T** is the most specialized → it wins → 2. (Cell G.)
Why this step? Deeper nesting = stronger constraint = higher on the specialization ladder, so the most-nested pattern always wins when it fits.
Verify: ids are 0, 1, 2 — a clean staircase as the pointer nesting deepens. ✓
Worked example Example 7 — Cell H: the boundary type
void and void*
template < class T > struct V { static const char* w (){ return "generic" ; } };
template < class T > struct V < T * > { static const char* w (){ return "ptr" ; } };
V < void >:: w (); // ?
V < void* >:: w (); // ?
Forecast: void is a type you can never make an object of. Does the partial still see void*?
V<void>: run the gate. void is not a pointer, so T* cannot match (there is no T with T* = void). Only primary → "generic". (Cell H, degenerate input.)
Why this step? Even a "weird" boundary type like void obeys the same shape-gate test as any ordinary type — it simply fails the pointer pattern, and we must confirm that before assuming anything special happens.
V<void*>: void* is a pointer, matching T* with T = void. The fact that you can't dereference void* is irrelevant to pattern matching — the shape is all that counts. Partial wins → "ptr". (Cell H, limiting.)
Why this step? Specialization matches type structure , not "usability" of the type.
Verify: "generic" for void, "ptr" for void*. The void boundary behaves exactly like any non-pointer; void* behaves exactly like any pointer. No special-casing needed. ✓
Worked example Example 8 — Cell I + B: real-world word problem, a per-type serializer
A logging library must turn any value into a string tag. Numbers give "num", C-strings give "cstr", and any pointer gives "ptr:" followed by the pointee's tag. Design it and predict the tags for int, const char*, and double*.
template < class T > struct Tag { static string of (){ return "num" ; } };
template <> struct Tag < const char* > { static string of (){ return "cstr" ; } };
template < class T > struct Tag < T * > { static string of (){ return "ptr:" + Tag < T >:: of (); } };
Forecast: What does Tag<double*>::of() return? (Careful — it recurses .)
Tag<int>: run the gates — no pointer shape, and it is not const char* → primary → "num".
Why this step? Filtering first confirms neither special candidate applies, so the generic "num" is correct rather than an accident.
Tag<const char*>: both the full spec <const char*> and the partial T* (with T=const char) match. Full beats partial → "cstr". (Cell B inside a real design.)
Why this step? We wanted C-strings treated as text, not as "pointer to char", so the full spec must outrank the partial — and it does, automatically.
Tag<double*>: matches T* with T=double; full spec doesn't apply. Partial fires → "ptr:" + Tag<double>::of() → "ptr:" + "num" → "ptr:num". (Cell I, with recursion via the partial.)
Why this step? The partial calls back into Tag<T> for the pointee, so specialization composes like a little type-level program.
Verify: Tag<int>::of() == "num", Tag<const char*>::of() == "cstr", Tag<double*>::of() == "ptr:num". The full spec cleanly wins for const char*, preventing it from being mis-tagged as a pointer. ✓ Related design tools: tag dispatch .
Worked example Example 9 — Cell J: the exam trap — "partially specialize a function"
template < class T > void f ( T ) { /* generic */ }
// ATTEMPT (illegal) — partial specialization of a function template:
// template<class T> void f<T*>(T*) { /* pointer version */ }
// ^^^^^ <-- compiler: "function template partial
// specialization is not allowed"
// FIX — write a second OVERLOAD instead (no <...> after the name):
template < class T > void f ( T * ) { /* pointer overload */ }
Forecast: Why does the commented-out line fail but the last line succeed, even though they look almost identical?
Read the illegal line's syntax. f<T*>(T*) puts a pattern in angle brackets after the function name — that is the syntax of a partial specialization . For classes this is legal; for functions it is not.
Why this step? You must first recognise which language feature the code is trying to use, because the error is not about pointers at all — it's about the f<...> form being forbidden on functions.
Recall the rule. Function templates support FULL specialization only, never PARTIAL. So template<> void f<int*>(int*){} (a full spec, empty <>) would be legal, but template<class T> void f<T*>(T*){} (a partial spec, T still free) is ill-formed and the compiler rejects it.
Why this step? Naming the exact rule turns a mysterious error message into a predictable one — this is the single fact the exam is testing.
Understand why the rule exists. Functions already have overload resolution , which ranks candidate functions. Adding partial specialization would create a second , competing ranking system, so the committee left partial specs out of functions. Classes have no overloading, so they need partial specialization instead.
Why this step? Knowing the reason means you'll never be tempted to "fix" the error by tweaking syntax — the mechanism itself is the wrong tool.
Apply the fix. The last line, template<class T> void f(T*), has no <...> after f, so it is not a specialization at all — it is a brand-new overload . For a call like f(somePtr), overload resolution finds f(T*) a more specialized match than f(T) and picks it, giving exactly the pointer-specific behaviour we wanted.
Why this step? This delivers the behaviour of a partial spec through the mechanism functions actually support.
Verify: The commented f<T*> form is a compile error ("function template partial specialization is not allowed"); the overload form compiles and dispatches pointer arguments to f(T*). Assertion: functions → full spec or overload only; partial function spec is ill-formed. ✓ (Outcome is a compile-time rule, not a numeric value.)
Worked example Example 10 — Cell K: limiting shapes — reference vs pointer vs array all distinct
template < class T > struct Kind { static int id (){ return 0 ; } }; // scalar/other
template < class T > struct Kind < T * > { static int id (){ return 1 ; } }; // pointer
template < class T > struct Kind < T & > { static int id (){ return 2 ; } }; // lvalue reference
template < class T , int N > struct Kind < T [ N ]>{ static int id (){ return 3 ; } }; // fixed array
Kind < int >:: id (); // ?
Kind < int* >:: id (); // ?
Kind < int& >:: id (); // ?
Kind < int [ 4 ]>:: id (); // ?
Forecast: Four different type "shapes" — predict all four ids before reading on.
int: run every gate. It is not a pointer, not a reference, not an array, so none of the three shape patterns open → primary → 0.
Why this step? Filtering first proves the plain scalar slips through every special gate and lands on the fallback — nothing exotic happens.
int*: only the T* gate opens (with T=int); reference and array gates stay shut. One survivor besides primary, and it's more specialized → 1.
Why this step? Each shape pattern gates on a different structure, so we check them one by one and keep only the one that opens.
int&: only the T& gate opens → 2. A reference is a distinct shape ; it is not a pointer, so the T* gate does not open for it.
Why this step? It is a common muddle to think "reference ≈ pointer"; the gates prove they are separate shapes with separate patterns.
int[4]: the array gate T[N] opens with T=int and N=4 → 3. Note the partial introduces a non-type parameter N (an integer), letting one pattern match a whole family of array sizes.
Why this step? Partial specs can pin structure that includes compile-time constants like the array length, so int[4], int[10], … all fire the same partial.
Verify: ids are 0, 1, 2, 3 — four disjoint shapes, four distinct answers, zero overlap and zero ambiguity. This is the "spanning" case: each partial covers its own corner of type-space, so every input has exactly one clear winner. ✓
Mnemonic The one rule that resolves every example above
F.P.P. — Full > Partial > Primary , and among partials, narrower wins . If two are equally narrow → ambiguity . Every single example on this page is that rule applied to a different shape of X.
Recall For
int**, rank which of T, T*, T** wins and why
All three match; T** is a strict subset of T*, which is a strict subset of T. ::: T** wins — it is the most specialized (deepest nesting = strongest constraint).
Recall Does
V<void*> fire the T* partial? Why or why not?
Specialization matches type structure , not usability. ::: Yes — void* has pointer shape (T* with T=void), so the partial fires even though you can't dereference void*.
Recall Why is
Tag<const char*> tagged "cstr" not "ptr:cstr"?
Because a full specialization outranks a matching partial. ::: The full spec Tag<const char*> beats the partial Tag<T*>, so it never recurses into the pointer branch.
Recall Ex 5 gives an ambiguity error. What single line fixes it?
Add something strictly more specialized than both partials. ::: template<> struct M<int,int>{}; — a full spec beats both partials, breaking the tie.
Recall For
int[4] and int&, why is there never an ambiguity between the array and reference partials?
Their shape gates are disjoint. ::: A reference is not an array and an array is not a reference, so each input opens exactly one gate — only one partial ever survives.
Related deep tools worth a visit: SFINAE and enable_if , constexpr if , and the container quirk std::vector<bool> special case .