Template specialisation

A template describes the general case, and sometimes one type deserves a different implementation. A full specialisation writes that implementation for exactly one set of arguments, and the compiler picks it whenever the arguments match.

The standard library specialises std::vector<bool> to pack bits and std::hash for every type it can hash. This lesson writes the three forms: a full class specialisation, a function specialisation and a partial specialisation that still leaves a parameter open.

A full specialisation

template <> class Box<const char*> with empty angle brackets is a full specialisation: it replaces the general Box for one exact type. The compiler chooses it when the argument is exactly const char*, and the general template for everything else.

The program stores a C string in a Box and prints through the specialised print(), which shows the pointer rather than the text. The general version would print the address, so the specialisation exists to print the characters.

A specialisation must be declared before the first use that would instantiate the general template. The last section shows the error when it comes too late.

The specialisation does not have to look like the general template. It can have different data members and different member functions, as long as it provides the operations the callers use; the compiler only checks the calls that are made.

#include <iostream> template <typename T> class Box { public: explicit Box(T value) : value_(value) {} void print() const { std::cout << value_ << '\n'; } private: T value_; }; template <> class Box<const char*> { public: explicit Box(const char* value) : value_(value) {} void print() const { std::cout << "text: " << value_ << '\n'; } private: const char* value_; }; int main() { Box<int> number{42}; Box<const char*> text{"hello"}; number.print(); text.print(); }

g++ -std=c++23 -Wall -Wextra full.cpp -o full
./full

42
text: hello

Specialising a function

A function template is specialised with the same empty angle brackets and the exact parameter types. The general version handles arithmetic, and the specialisation prints a message for const char*.

The program calls show with an int and with a string. The int goes through the template, the string through the specialisation, and the two output lines differ.

Prefer an ordinary overload to a function specialisation where you can: overloads take part in normal overload resolution, while a specialisation only applies when the template would be instantiated with exactly those arguments. The next lesson's tag dispatch is the other standard alternative.

#include <iostream> template <typename T> void show(T value) { std::cout << "value " << value << '\n'; } template <> void show<const char*>(const char* value) { std::cout << "text " << value << '\n'; } int main() { show(3); show("hello"); }

g++ -std=c++23 -Wall -Wextra function.cpp -o function_spec
./function_spec

value 3
text hello

A partial specialisation

A partial specialisation fixes part of the parameter list and leaves the rest open. template <typename T> class Box<T*> matches every pointer type and still has a T to work with.

The program stores an int* and a double* in boxes and prints the pointed-to value through the partial specialisation. The general template is still used for the plain int box.

Partial specialisation exists for class templates only; a function template cannot be partially specialised and uses overloading instead. That asymmetry is one of the reasons the standard library puts traits in class templates.

The partial specialisation also shows why the general template keeps its body: Box<int> still uses it, and only the pointer forms are replaced.

#include <iostream> template <typename T> class Box { public: explicit Box(T value) : value_(value) {} void print() const { std::cout << "value " << value_ << '\n'; } private: T value_; }; template <typename T> class Box<T*> { public: explicit Box(T* value) : value_(value) {} void print() const { std::cout << "pointer to " << *value_ << '\n'; } private: T* value_; }; int main() { int number = 7; double real = 2.5; Box<int> plain{3}; Box<int*> pointer{&number}; Box<double*> real_pointer{&real}; plain.print(); pointer.print(); real_pointer.print(); }

g++ -std=c++23 -Wall -Wextra partial.cpp -o partial_spec
./partial_spec

value 3
pointer to 7
pointer to 2.5

A trait built from a specialisation

The standard type traits are class templates with specialisations. A trait that answers a question about a type is a template with a bool value, true in the general case and false in a specialisation, or the other way round.

The program's is_pointer trait is true only for the pointer specialisation. The static_assert lines check both answers at compile time, and the printed value shows the same answer at run time.

This is the technique behind lesson 042: a primary template, one or more specialisations, and a _v helper that reads the value.

#include <iostream> template <typename T> struct is_pointer { static constexpr bool value = false; }; template <typename T> struct is_pointer<T*> { static constexpr bool value = true; }; template <typename T> constexpr bool is_pointer_v = is_pointer<T>::value; int main() { static_assert(!is_pointer_v<int>); static_assert(is_pointer_v<const char*>); std::cout << is_pointer_v<int> << ' ' << is_pointer_v<double*> << '\n'; }

g++ -std=c++23 -Wall -Wextra trait.cpp -o trait
./trait

0 1

A specialisation after the first use

Once the compiler has instantiated the general template for a type, a later specialisation of that type is too late. The program below uses Box<int> in main and declares the specialisation afterwards, and the compiler rejects the declaration.

The message says the specialisation comes after instantiation, and the fix is to move it above every use, usually into the header where the general template lives.

The same rule applies to function templates. A specialisation is not a new overload that can appear anywhere; it changes what an existing template produces, so it has to be visible before the template is used.

#include <iostream> template <typename T> class Box { public: explicit Box(T value) : value_(value) {} void print() const { std::cout << value_ << '\n'; } private: T value_; }; int main() { Box<int> b{3}; b.print(); } template <> class Box<int> { public: explicit Box(int value) : value_(value) {} void print() const { std::cout << "special " << value_ << '\n'; } private: int value_; };

g++ -std=c++23 -Wall -Wextra late.cpp -o late

late.cpp:25:7: error: specialization of 'Box<int>' after instantiation
25 | class Box<int>
| ^~~~~~~~
late.cpp:25:7: error: redefinition of 'class Box<int>'
late.cpp:4:7: note: previous definition of 'class Box<int>'
4 | class Box
| ^~~

Exercises

  1. Add a full specialisation of Box for std::string that prints the length instead of the text.
  2. Write a partial specialisation Box for references and decide what print() should show.
  3. Move the specialisation in late.cpp above main and run the program. Which line does main print now?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is a full specialisation?

    Answer

    An implementation for one exact set of template arguments

    The compiler prefers the specialisation when the arguments match it exactly.

  2. What is a partial specialisation?

    Answer

    An implementation for a family of arguments, such as every pointer

    The pattern T* matches every pointer while leaving T open.

  3. Why does the standard library specialise std::hash for a type?

    Answer

    So the type can be used as a key in an unordered container

    The unordered containers look up a hash function, and the specialisation supplies one.

  4. Where must a specialisation appear?

    Answer

    Before the first use that would instantiate the primary template

    Once the primary template is instantiated for those arguments, a later specialisation is too late.

  5. What does this program print? #include <iostream> #include <string> template <typename T> struct printer { static T show(T value) { return value; } }; template <> struct printer<int> { static int show(int value) { return value; } }; int main() { std::cout << printer<int>::show(42) << ' ' << "text: " << printer<std::string>::show("hello"); }

    Answer

    42 text: hello

    The int specialisation returns 42, and the generic template answers for the string.

  6. What does this program print? #include <iostream> #include <string> template <typename T> void show(const T&) { std::cout << "value 3\n"; } template <> void show(const std::string&) { std::cout << "text hello\n"; } int main() { show(3); show(std::string{"hello"}); }

    Answer

    value 3 / text hello

    The generic template prints the first line, and the string specialisation prints the second.

  7. What does this program print? #include <iostream> template <typename T> struct describe { static void show(const T& value) { std::cout << "value " << value; } }; template <typename T> struct describe<T*> { static void show(T* p) { std::cout << "pointer to " << *p; } }; int main() { int number = 3; int seven = 7; double real = 2.5; describe<int>::show(number); std::cout << " / "; describe<int*>::show(&seven); std::cout << " / "; describe<double*>::show(&real); }

    Answer

    value 3 / pointer to 7 / pointer to 2.5

    The int uses the primary template and every pointer uses the partial specialisation.

  8. What do the two trait results print?

    Answer

    0 1

    The trait answers false for the int and true for the pointer, and the program prints the two answers.

  9. This code is compiled. What does the compiler report? template <typename T> struct Box {}; Box<int> first; template <> struct Box<int> { int value = 0; }; int main() { Box<int> second; return second.value; }

    Answer

    specialization of 'Box<int>' after instantiation

    The primary template was instantiated for int by the first variable, so the later specialisation is rejected.

  10. Why does a function template need an ordinary function for a special case?

    Answer

    A function specialisation cannot be partially specialised

    Only class templates support partial specialisation, so a function uses an overload instead.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev