Skip to content
jieqi's archive
Go back

C++: Class Templates - Full and Partial Specialisation

Contents

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 templateClass template
DeductionAlways, from the call argumentsNever, pre-C++17. CTAD deduces from constructor args, but it’s a separate, weaker algorithm
Explicit argsUsually optionalMandatory, except under CTAD
Partial specializationIllegalLegal, and the main tool
OverloadingYes, and it’s how you get specialization-like behaviourNo such thing. Class names don’t overload
SelectionOverload resolution at the call site, competing with every visible fMatched against specializations at instantiation, in isolation
Instantiation granularityWhole function or nothingLazy per member. Unused members are never instantiated
Member templatesn/aMembers 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

WhatCode
Primarytemplate<class T, class U> struct P { };
Full specializationtemplate<> struct P<int, char> { };
Partial: pin one paramtemplate<class U> struct P<int, U> { };
Partial: match a patterntemplate<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

WhatCodeLegal
Primarytemplate<class T, class U> void f(T, U);yes
Full specializationtemplate<> void f(int, char);yes, avoid
Partial specializationtemplate<class T> void f<T, T>(T, T);no
Overload insteadtemplate<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

CodeMeaning
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 constexpr and 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_member and two-phase lookup now appear nowhere.

Editorial

  • “override” in Full Specialisation collides with the override keyword from the Virtual Dispatch post. “replace” or “specialise” sits closer to the series.
  • Two different Pair templates. 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’s Pair<int>::larger().
  • Voice pass on the intro paragraph under the first Pair block and the per-TU collapsible.
  • description in the frontmatter is still -.
Previous Post
C++: Lambda Basics
Next Post
C++: Concurrency Pt 1 - Threads