5.2.15 · D2C++ Programming

Visual walkthrough — Template specialization — full and partial

2,086 words9 min readBack to topic

We build up one idea at a time. Nothing below assumes you memorised anything from the parent note — we re-earn every word.


Step 1 — A template is a stamp that makes many boxes

WHAT. Start with the most generic thing: a primary template.

template<class T>
struct Box { /* generic recipe */ };

Read template<class T> out loud as: "for any type you hand me, called T, here is how to build a Box." The word struct just means "a little bundle of data and functions." T is a blank slot — a placeholder waiting for a real type like int or double.

WHY. The compiler cannot ship a box for every type in advance — there are infinitely many types. So instead it ships a stamp: a machine that, when you write Box<int>, presses out a fresh Box with the word int filling the T slot. This pressing is called instantiation.

PICTURE. The stamp on the left; three pressed boxes on the right, each with a different type poured into the slot.

Figure — Template specialization — full and partial

Step 2 — Every type is a point; a template covers a REGION of types

WHAT. Imagine drawing all possible types as dots on a big sheet — int, double, int*, char*, pair<int,char>, and so on. Now draw a boundary around the set of types a given stamp is willing to serve. We call that boundary the stamp's match region.

WHY. The whole selection problem becomes visual once we think in regions. The primary template<class T> serves everything — its match region is the entire sheet. A specialization will draw a smaller region inside it. "Which recipe wins" turns into "which region is smallest around the point X."

PICTURE. The full sheet of type-dots. A dashed outer rectangle labelled "primary T — matches ALL types" wraps every dot.

Figure — Template specialization — full and partial

Step 3 — Full specialization: a region containing exactly ONE point

WHAT. A full specialization pins all parameters to concrete types, so its region shrinks to a single dot.

template<>            // no slots left — zero free parameters
struct Box<int> {     // this exact type: int
    /* custom recipe for int only */
};

Term by term:

  • template<> — the angle brackets are empty because you filled in every slot; there is nothing left for the compiler to deduce.
  • Box<int> — names the one instantiation you are replacing. This is the dot.

WHY. Sometimes the generic recipe is wrong, slow, or impossible for exactly one type (recall std::vector<bool> bit-packing from the parent). Full specialization lets you hijack that single point without renaming anything — callers still write Box<int>.

PICTURE. The sheet from Step 2, now with a tiny circle lassoing the single int dot, labelled "full spec Box<int> — 1 type". The double and int* dots stay outside, still served by the primary.

Figure — Template specialization — full and partial

Step 4 — Partial specialization: a region shaped like a FAMILY

WHAT. A partial specialization pins only the shape of the type, leaving some slots free.

template<class T>     // T is still a free slot
struct Box<T*> {      // but the type must look like "pointer to something"
    /* custom recipe for any pointer */
};

Term by term:

  • template<class T> — one slot remains free; that is what makes it partial rather than full.
  • Box<T*> — the pattern. The * means "matches any type of the form pointer-to-T." int* matches with T = int; char* matches with T = char; int does not match (no *).

WHY. Often you want one custom recipe for a whole family — "all pointers," "all pair<A,B> — not a single type and not everything. Since function overload resolution already handles function families, C++ only allows partial specialization on class templates (the parent's mistake-callout: functions get overloads instead).

PICTURE. The sheet again, now with a medium oval enclosing every *-dot (int*, char*, double*), labelled "partial spec T* — the pointer family". It sits inside the primary rectangle but is bigger than any single point.

Figure — Template specialization — full and partial

Step 5 — The selection rule, as a walk from outside in

WHAT. Put a query type X as a star on the sheet. The compiler collects every stamp whose region contains the star, then keeps the smallest such region.

WHY. "Smallest region containing the star" is precisely the plain-English rule "most specialized match wins." A full spec (single point) beats a partial (family), which beats the primary (everything) — but only among those that actually contain the star.

PICTURE. For X = int*: the star lands inside the primary rectangle and inside the T* oval, but outside the Box<int> point-circle. Two nested regions contain it; the inner T* oval wins. Arrows show the compiler "shrinking inward" until no smaller region contains the star.

Figure — Template specialization — full and partial

Step 6 — Comparing two regions: partial ordering

WHAT. When you have several partials, "smaller region" needs a precise test, called partial ordering. Spec A is more specialized than B if every type matching A also matches B, but not the reverse.

WHY. "Smaller" was intuitive on the sheet; the compiler needs a mechanical version. Partial ordering is exactly "A's region B's region and they are not equal" — set containment, but computed by a matching game rather than by drawing.

PICTURE. Two ovals. pair<int,B> (fix the first slot to int, second free) sits inside pair<A,B> (both free). Every pair<int, something> is also a pair<A,B>, but not every pair<A,B> has int first — so the inner one is more specialized.

Figure — Template specialization — full and partial

Step 7 — The degenerate case: two regions that CROSS (ambiguity)

WHAT. What if two partials overlap but neither sits fully inside the other? Then for a type in the overlap, there is no smallest region — and the compiler refuses to guess.

template<class A, class B> struct M {};
template<class A> struct M<A, int> {};   // region: "second slot is int"
template<class B> struct M<int, B> {};   // region: "first slot is int"
 
M<int, int> m;   // ERROR: ambiguous

Term by term:

  • M<A,int> covers everything whose second parameter is int.
  • M<int,B> covers everything whose first parameter is int.
  • M<int,int> lives in both regions at once — the crossing zone.

WHY. Neither region is a subset of the other (each contains points the other misses: M<char,int> is only in the first; M<int,char> only in the second). So partial ordering gives no winner. Rather than pick arbitrarily, the compiler emits an ambiguity error — a feature, protecting you from silent wrong choices.

PICTURE. Two overlapping ovals forming a Venn diagram. The lens-shaped overlap holds the single dot M<int,int>, flagged in amber with "AMBIGUOUS — no smallest region."

Figure — Template specialization — full and partial

The one-picture summary

Everything above is one idea: nest the regions, drop the star, keep the innermost region that still contains it. Full spec = a point, partial spec = a shaped family, primary = the whole sheet. Overlapping-but-not-nested regions with the star in the overlap = ambiguity.

Figure — Template specialization — full and partial
Recall Feynman: retell the whole walkthrough in plain words

Picture a giant sheet of paper with a dot for every type in existence. The generic template is a lazy fence drawn around all the dots — it'll serve anyone. A full specialization is a tiny lasso around exactly one dot: "for int, use my recipe." A partial specialization is a medium loop around a family of dots that share a shape — "all pointers," "all pairs." Now someone asks for a type: drop a star on that type's dot. The compiler looks at every fence, loop, and lasso that surrounds the star and keeps the tightest one — because the tightest region means "you clearly meant this special case." A one-dot lasso beats a family loop beats the everything-fence. The only way it breaks is if two loops cross and the star sits in the crossing region: now there's no single tightest loop, so the compiler throws up its hands and says "ambiguous" rather than guess. Fix it by drawing an even tinier lasso — a full spec — right on the disputed dot.

Recall

Which region-shape is a full specialization? ::: A single point — all parameters pinned. Which region-shape is a partial specialization? ::: A shaped family — some parameters free, the type's structure constrained. Winner among matching regions? ::: The smallest region still containing the query type. Two partials overlap but neither is inside the other, and X is in the overlap — result? ::: Ambiguity error; no most-specialized choice exists. Formal test for "A more specialized than B"? ::: match(A) ⊆ match(B) and match(A) ≠ match(B).


Related vault topics: Templates — function and class basics · Type Traits and std::is_pointer · SFINAE and enable_if · Overload Resolution vs Specialization · std::vector<bool> special case · Tag Dispatch and Policy-based Design · constexpr if (C++17) — alternative to specialization