Intuition How to use this page
Work each problem before opening its solution. The problems climb a ladder: first you just recognize which recipe fires, then you apply the rules, analyze tricky selection, synthesize real traits, and finally master the edge cases. This page tests the ideas from the parent note .
Vocabulary reminder (all built in the parent note):
Primary template — the generic fallback recipe.
Full specialization — template<> with the exact type pinned, e.g. Box<int>.
Partial specialization — a shape pinned (like T* or pair<A,B>), some params still free.
The compiler picks the most specialized matching pattern; ties with no winner are an ambiguity error .
Worked example Exercise 1.1
Classify each declaration as primary , full , or partial :
template < class T > struct A { }; // (a)
template <> struct A < int > { }; // (b)
template < class T > struct A < T * > { }; // (c)
template < class A2 , class B > struct C { }; // (d)
template < class B > struct C < int , B > { }; // (e)
Recall Solution 1.1
(a) primary — one free parameter T, no pattern on the name.
(b) full — template<> (zero free params), exact type int pinned.
(c) partial — T is still free but the shape T* is pinned.
(d) primary — two free params, no pattern.
(e) partial — first param pinned to int, B still free.
Worked example Exercise 1.2
Which of these is illegal C++, and why?
template < class T > void f ( T ) { } // primary
template <> void f < int >( int ) { } // (i)
template < class T > void f < T * >(T * ) { } // (ii)
Recall Solution 1.2
(i) is legal — function templates can be fully specialized.
(ii) is illegal — function templates cannot be partially specialized. You would use an overload template<class T> void f(T*) instead (see Overload Resolution vs Specialization ).
Common mistake L1 trap — "empty
<> means generic"
Why it feels right: the angle brackets look empty like they hold "nothing special."
The truth: template<> means the opposite — zero free parameters remain , so it is a full specialization, the most specific form possible.
Fix: read template<> as "I filled in everything," and read the <int> after the name as "for this exact type."
Worked example Exercise 2.1
Given:
template < class T > struct S { static int v (){ return 0 ; } };
template <> struct S < char > { static int v (){ return 1 ; } };
template < class T > struct S < T * > { static int v (){ return 2 ; } };
What does each print?
S < double >:: v (); // (a)
S < char >:: v (); // (b)
S < char* >:: v (); // (c)
S < int** >:: v (); // (d)
Recall Solution 2.1
(a) double matches no pattern → primary → 0.
(b) char matches the full spec S<char> → 1.
(c) char* — does it match S<char>? No, char* is not char. It matches S<T*> with T=char → 2.
(d) int** is a pointer (to int*), so S<T*> fires with T=int* → 2.
Answers: 0, 1, 2, 2.
Worked example Exercise 2.2
Write a partial specialization of
template < class T > struct Unwrap { using type = T ; };
so that Unwrap<T*>::type gives back T (the pointed-to type). Then state what Unwrap<int**>::type is.
Recall Solution 2.2
template < class T > struct Unwrap < T * > { using type = T ; };
For Unwrap<int**>: the pattern T* matches with T = int*, so ::type is int* — it strips one level of pointer, not all. To strip fully you would recurse, but that is beyond this single spec.
Common mistake L2 trap — thinking
T* unwraps all pointers at once
Why it feels right: the word "pointer" feels like a single category.
The truth: ==T* peels exactly one layer==. For int**, the match is T = int*, leaving one * behind.
Fix: each application of the T* pattern removes one star. Chained stripping needs recursion.
Worked example Exercise 3.1
template < class A , class B > struct M { static int id (){ return 0 ; } };
template < class A > struct M < A , int > { static int id (){ return 1 ; } };
template < class B > struct M < int , B > { static int id (){ return 2 ; } };
template <> struct M < int , int >{ static int id (){ return 3 ; } };
What does each line do — compile fine, or error?
M < double , char >:: id (); // (a)
M < double , int >:: id (); // (b)
M < int , double >:: id (); // (c)
M < int , int >:: id (); // (d)
Recall Solution 3.1
(a) <double,char> matches only the primary → 0.
(b) <double,int> matches M<A,int> only → 1.
(c) <int,double> matches M<int,B> only → 2.
(d) <int,int> matches M<A,int>, M<int,B>, and M<int,int> . But the full specialization M<int,int> is the most specific of all and wins outright → 3.
Answers: 0, 1, 2, 3. Note: without line for M<int,int> (the full spec), case (d) would be an ambiguity error because M<A,int> and M<int,B> tie.
Worked example Exercise 3.2
Given only the primary plus these two partials (no full spec):
template < class A , class B > struct N { };
template < class A > struct N < A , int > { }; // P1
template < class T > struct N < T , T > { }; // P2
Is N<int,int> ambiguous? Justify using partial ordering.
Recall Solution 3.2
Both P1 (<A,int>) and P2 (<T,T>) match N<int,int>. To break the tie, ask: is one pattern strictly more specialized (matches a subset of the other)?
P1 matches every <X,int>; P2 matches every <X,X>. Neither set contains the other (e.g. <double,int> matches P1 not P2; <char,char> matches P2 not P1).
So neither is more specialized → ambiguity error .
Common mistake L3 trap — "the full spec is just another partial, so it can tie"
Why it feels right: in (d) three things matched, and two of them tie, so surely the whole thing is ambiguous.
The truth: a full specialization always beats every partial (step 2 of the selection rule before partial ordering is even consulted). Only partials among themselves can be ambiguous.
Fix: apply the rule in order — full spec first, then partial ordering, then primary.
Worked example Exercise 4.1
Build a trait RemoveConst<T> such that RemoveConst<const X>::type is X and RemoveConst<X>::type is X. Write the primary and the partial, then give RemoveConst<const int>::type and RemoveConst<int>::type.
Recall Solution 4.1
template < class T > struct RemoveConst { using type = T ; };
template < class T > struct RemoveConst < const T > { using type = T ; };
RemoveConst<const int> matches the partial const T with T=int → ::type = int.
RemoveConst<int> matches only the primary → ::type = int.
This is exactly how std::remove_const is built — see Type Traits and std::is_pointer .
Worked example Exercise 4.2
Write Rank<T> giving the number of array dimensions:
Rank<int>::value == 0
Rank<int[]>::value == 1
Rank<int[][3]>::value == 2
Use two partial specializations (T[] and T[N]) that recurse. Trace Rank<int[][3]>::value.
Recall Solution 4.2
template < class T > struct Rank { static const int value = 0 ; };
template < class T > struct Rank < T []> { static const int value = 1 + Rank < T >::value; };
template < class T , unsigned N > struct Rank < T [ N ]> { static const int value = 1 + Rank < T >::value; };
Trace for int[][3] (an array of int[3] , unknown outer size):
Matches Rank<T[]> with T = int[3] → value = 1 + Rank<int[3]>::value.
Rank<int[3]> matches Rank<T[N]> with T=int, N=3 → value = 1 + Rank<int>::value.
Rank<int> matches primary → 0.
Unwind: 1 + (1 + 0) = 2. ✓
Common mistake L4 trap — one partial for all array kinds
Why it feels right: int[] and int[3] both "are arrays."
The truth: the compiler treats unknown-bound T[] and known-bound T[N] as different patterns ; you need both partial specs or one kind of array silently falls through to the primary and reports rank 0.
Fix: cover every shape explicitly — this is contract rule 4, "cover all cases."
Worked example Exercise 5.1
A student writes:
template < class T > struct Traits { static const int id = 0 ; };
Traits <int> a; // first use
template <> struct Traits < int > { static const int id = 9 ; }; // spec AFTER use
What is wrong, and what value would a.id conceptually have "wanted" vs the actual rule?
Recall Solution 5.1
Ill-formed program. A specialization must appear before the type's first instantiation. Here Traits<int> is instantiated at Traits<int> a;, then specialized — the compiler already committed to the primary (id = 0). The correct value the student intended was 9, but the ordering violation makes the program ill-formed (a diagnostic is required).
Fix: move the template<> specialization above Traits<int> a;.
Worked example Exercise 5.2
Compare these two ways to give int special behaviour. Which compiles, and what does each print for pick<int>() and pick<double>()?
// Way A: full function specialization
template < class T > int pick (){ return 0 ; }
template <> int pick < int >(){ return 1 ; }
// Way B (separate program): overload with a tag
template < class T > int pick2 ( T ){ return 0 ; }
int pick2 ( int ){ return 1 ; }
Recall Solution 5.2
Way A is legal (full spec of a function template is allowed). pick<int>() → 1, pick<double>() → 0.
Way B relies on overload resolution : pick2(int) (a non-template exact match) beats the template for int. pick2(5) → 1, pick2(5.0) → 0.
Best practice for functions is Way B (overloading), because partial-like behaviour on functions must use overloads, and overloads compose more predictably. See Overload Resolution vs Specialization and Tag Dispatch and Policy-based Design .
Worked example Exercise 5.3
Decide which fires for each call. Given:
template < class T > struct Fmt { static const int c = 0 ; }; // primary
template < class T > struct Fmt < T * > { static const int c = 1 ; }; // pointer
template <> struct Fmt < int* >{ static const int c = 2 ; }; // exact int*
Evaluate Fmt<double>::c, Fmt<double*>::c, Fmt<int*>::c.
Recall Solution 5.3
Fmt<double> → no pattern but primary → 0.
Fmt<double*> → matches Fmt<T*> (T=double); no full spec for double* → 1.
Fmt<int*> → matches primary? no. Fmt<T*>? yes. Fmt<int*> full spec? yes — full wins → 2.
Answers: 0, 1, 2.
Common mistake L5 trap — forgetting the same-namespace / before-use rules
Why it feels right: "it's the same type, the compiler will find it eventually."
The truth: a specialization must be visible before first instantiation and live in the primary's namespace . Violations are ill-formed even when they "obviously" mean the right thing.
Fix: keep specializations right next to the primary, before any use. This is the discipline that makes trait libraries like Type Traits and std::is_pointer reliable.
Recall Master check — do it from memory
Selection order, one line ::: full spec first → most-specialized partial → primary → else ambiguity if partials tie.
Fmt<int*> with primary, T*, and full int* ::: full int* wins.
Rank<int[][3]>::value ::: 2.
Why Way B for functions ::: functions can't be partially specialized; overloads give the same effect predictably.