Lesson 4 of 8
A template says "the same code for every type". Sometimes one type needs different code. Specialization provides it.
Full specialization of a function
#include <cstring>
#include <iostream>
#include <string>
// The general case.
template <typename T>
void describe(const T& value) {
std::cout << "generic: " << value << "\n";
}
// Full specialization for bool: print true/false instead of 1/0.
template <>
void describe<bool>(const bool& value) {
std::cout << "bool: " << (value ? "true" : "false") << "\n";
}
// Full specialization for const char*: report the length, since the generic
// version would print the text and say nothing useful about it.
template <>
void describe<const char*>(const char* const& value) {
std::cout << "c-string of length " << std::strlen(value) << ": " << value << "\n";
}
int main() {
describe(42);
describe(3.5);
describe(std::string("text"));
describe(true);
const char* literal = "hello";
describe(literal);
return 0;
}
generic: 42
generic: 3.5
generic: text
bool: true
c-string of length 5: hello
The syntax is template <> — an empty parameter list, because nothing is left generic — followed
by the type in <> after the name.
Note the second specialization's parameter: const char* const&. Substituting T = const char*
into const T& gives reference to const pointer to const char, so the const appears twice.
Getting this wrong is a common cause of "specialization does not match any template" errors.
Full and partial specialization of a class
Classes support both. This is where specialization is actually useful:
#include <iostream>
#include <string>
// General class template.
template <typename T>
class Box {
T value;
public:
explicit Box(T v) : value(std::move(v)) {}
void report() const { std::cout << "Box holding one value: " << value << "\n"; }
};
// Full specialization: a completely different implementation for bool.
template <>
class Box<bool> {
bool value;
public:
explicit Box(bool v) : value(v) {}
void report() const {
std::cout << "Box<bool> stored as one bit: " << (value ? "set" : "clear") << "\n";
}
bool flipped() const { return !value; } // a member the general template does not have
};
// Partial specialization: still a template, but narrowed to pointers.
template <typename T>
class Box<T*> {
T* value;
public:
explicit Box(T* v) : value(v) {}
void report() const {
if (value) {
std::cout << "Box<T*> pointing at " << *value << "\n";
} else {
std::cout << "Box<T*> holding nullptr\n";
}
}
};
int main() {
Box<int> a(42);
a.report();
Box<std::string> b("text");
b.report();
Box<bool> c(true);
c.report();
std::cout << std::boolalpha << "flipped: " << c.flipped() << "\n";
int n = 7;
Box<int*> d(&n);
d.report();
Box<int*> e(nullptr);
e.report();
return 0;
}
Box holding one value: 42
Box holding one value: text
Box<bool> stored as one bit: set
flipped: false
Box<T*> pointing at 7
Box<T*> holding nullptr
Three things to take from that.
Full specialization (template <> class Box<bool>) fixes every parameter. It is a separate
class that happens to share a name — note it has a flipped() member the general template does
not, and shares no code with it.
Partial specialization (template <typename T> class Box<T*>) fixes a pattern. It still
takes a parameter, so it covers Box<int*>, Box<std::string*>, and every other pointer at once.
Box<int*> deduces T = int, which is why report can print *value.
Anything you do not rewrite, you lose. The specializations are complete class definitions, not patches. That is the main cost of specialization and the reason to prefer other tools when they fit.
Functions cannot be partially specialized
// template <typename T>
// void process(T value);
//
// template <typename T>
// void process<T*>(T* value); // ERROR: no partial specialization of functions
Use an overload instead, which does the same job:
template <typename T>
void process(T value); // general
template <typename T>
void process(T* value); // overload for pointers — legal, and preferred
Prefer overloading to function specialization
That is not just style advice. Function specializations interact with overload resolution in a way that surprises everyone the first time:
#include <iostream>
template <typename T>
void handle(T) { std::cout << "1: template <typename T>\n"; }
template <typename T>
void handle(T*) { std::cout << "2: template <typename T> on T*\n"; }
// A specialization of #1, not of #2 — even though it is written for int*.
template <>
void handle<int*>(int*) { std::cout << "3: specialization of #1\n"; }
int main() {
int n = 0;
handle(&n); // which one?
return 0;
}
2: template <typename T> on T*
The specialization is never called. Here is why, and it is worth following carefully.
Overload resolution considers only the base templates — #1 and #2. Specializations do not
participate. Between those two, #2 is more specialised for an int*, so it wins. Only then does
the compiler ask "does #2 have a specialization for int*?" — and it does not, because #3
specialises #1.
Move #3 above #2 and it is called, because then it attaches to a different base template. Code whose behaviour depends on declaration order is code waiting to break.
Overloads have none of this problem. Overload functions; specialise classes.
The standard library's own cautionary tale
std::vector<bool> is a full specialization, and it is the most regretted decision in the C++
standard library.
The intent was reasonable: pack eight bools into one byte, for eight times less memory. The
consequence is that std::vector<bool> is not a std::vector:
#include <iostream>
#include <type_traits>
#include <vector>
int main() {
std::vector<char> chars(8, 'x');
std::vector<bool> bools(8, true);
std::cout << std::boolalpha;
// For every other type, operator[] returns a real reference.
std::cout << "vector<char>[0] is a real reference: "
<< std::is_same_v<decltype(chars[0]), char&> << "\n";
// For bool it returns a proxy object, because there is no such thing
// as a reference to a single bit.
std::cout << "vector<bool>[0] is a real reference: "
<< std::is_same_v<decltype(bools[0]), bool&> << "\n";
// So this does not compile:
// bool& r = bools[0];
// while this does:
bool copy = bools[0];
std::cout << "copied value: " << copy << "\n";
return 0;
}
vector<char>[0] is a real reference: true
vector<bool>[0] is a real reference: false
copied value: true
You cannot form a reference to a single bit, so operator[] returns a proxy object that
pretends to be a bool&. Consequences:
bool& r = v[0];does not compile, though it does for every other element type.&v[0]does not give you abool*.- Generic code written for
std::vector<T>can fail to compile atT = boolalone.
The lesson is the one the committee learned the hard way: a specialization that changes the interface rather than just the implementation breaks every generic user. Specialise to make a type work correctly, not to make it behave differently.
If you want packed bits, use std::bitset (fixed size) or std::vector<char> (if you want a real
container). If you want the interface, avoid std::vector<bool>.
When to specialise
Genuinely useful cases:
- A type the generic algorithm gets wrong.
const char*compared with<compares addresses (lesson 12.2), so it needsstrcmp. - A type with a much better implementation.
std::swapis specialised for containers to swap pointers rather than copy contents. - Type traits.
std::is_pointer<T>is nothing but a general template sayingfalseand a partial specialization forT*sayingtrue. This is the dominant real-world use.
Here is that pattern in full, since it explains most of <type_traits>:
// The general case: not a pointer.
// template <typename T>
// struct is_pointer { static constexpr bool value = false; };
//
// The partial specialization: anything matching T* is.
// template <typename T>
// struct is_pointer<T*> { static constexpr bool value = true; };
//
// is_pointer<int>::value -> false (general)
// is_pointer<int*>::value -> true (specialization matches)
Since C++20, a concept (lesson 12.7) expresses most of these more directly. Reach for specialization when you need a different implementation, and for a concept when you need a different requirement.
Try it
Write a template serialize(value) returning a std::string. Specialise it for bool (produce
"true"/"false"), and partially specialise a class version for pointers so a null prints
"null". Then write the same thing using overloads and compare which reads better.
Checkpoint
- Specialization gives one type — or one pattern of types — its own implementation.
template <>before the definition marks a full specialization.- Partial specialization (
Box<T*>) still takes a parameter and matches a pattern. - Functions cannot be partially specialised; use an overload.
- Specializations do not take part in overload resolution, so overload functions, specialise classes.
- A specialization is a complete definition — anything you do not rewrite is gone.
std::vector<bool>shows the danger: it changed the interface, not just the implementation.- The dominant real use is type traits: a general template plus a partial specialization.
Serializer<T>class plusserialize<T>()function is the idiom for a specialisable interface.
Questions about this lesson
Log in to ask a question.