Template Specialization
Full and partial specialization, explicit instantiation, specialization ordering, and when to prefer overloads — with C++ version discipline.
Template Specializationsince C++98Template specialization provides an alternative definition of a primary template for specific template arguments — either exact types (full specialization) or families of types matching a pattern (partial specialization, classes and variable templates only).
Overview
Template specialization is the mechanism by which a generic template is replaced with a hand-written implementation for particular arguments. The compiler performs pattern matching at instantiation time: it finds all full and partial specializations that match the requested arguments, then selects the most specialized match; if no specialization matches, the primary template is used.
Specialization is not the same as instantiation. Instantiation is the compiler generating code from a template for a specific argument set. Specialization is you, the programmer, supplying that code manually.
There are three distinct forms:
| Form | Applies to | Syntax |
|---|---|---|
| Full (explicit) specialization | class, function, variable templates | template<> |
| Partial specialization | class and variable templates only | template<typename T> with constrained argument list |
| Explicit instantiation | class and function templates | template class X<int>; |
Syntax
Full specialization:
template<>
struct TypeTraits<int> { /* ... */ }; // class
template<>
void serialize<MyType>(MyType&) { /* ... */ }; // functionPartial specialization (class/variable templates only):
template<typename T>
struct Storage<T*> { /* pointer specialization */ };
template<typename T, typename Alloc>
struct Container<T, Alloc, true> { /* third param fixed */ };Explicit instantiation:
extern template class std::vector<MyType>; // suppress in this TU
template class std::vector<MyType>; // emit in exactly one TUExamples
Full Specialization
Full specialization provides an exact match for one combination of template arguments. The template<> prefix is mandatory.
#include <string_view>
template<typename T>
struct TypeName { static constexpr std::string_view value = "unknown"; };
template<> struct TypeName<int> { static constexpr std::string_view value = "int"; };
template<> struct TypeName<double> { static constexpr std::string_view value = "double"; };
template<> struct TypeName<bool> { static constexpr std::string_view value = "bool"; };
// C++17: constexpr if is often cleaner for small type switches,
// but full specialization remains the right tool when the body is large
// or when you need the type to be a complete class with members.
static_assert(TypeName<int>::value == "int");
static_assert(TypeName<float>::value == "unknown");Partial Specialization — Pattern Matching
The compiler chooses the most specialized matching partial specialization. "Most specialized" means the pattern places the strictest constraints on the template arguments.
// Primary: generic pair
template<typename T, typename U>
struct Pair { using type = T; /* ... */ };
// Partial: both types identical
template<typename T>
struct Pair<T, T> { using type = T; /* can optimize */ };
// Partial: second type is a pointer
template<typename T, typename U>
struct Pair<T, U*> { using ptr_type = U; };
// Partial: both pointers — more specialized than Pair<T, U*>
template<typename T, typename U>
struct Pair<T*, U*> { using first_ptr = T; using second_ptr = U; };
Pair<int, double> a; // primary
Pair<int, int> b; // Pair<T, T>
Pair<int, float*> c; // Pair<T, U*>
Pair<int*, float*> d; // Pair<T*, U*> — wins (more constrained)If two partial specializations are equally specific for a given instantiation, the compiler emits an ambiguity error — no implicit tiebreaking.
Specialization for Pointers vs Values
A canonical pattern: store values by value, store pointees via unique ownership.
#include <memory>
template<typename T>
class Registry {
T value_;
public:
void set(T v) { value_ = std::move(v); } // C++11 move
T const& get() const { return value_; }
};
// Specialization: take ownership of the pointed-to object
template<typename T>
class Registry<T*> {
std::unique_ptr<T> ptr_; // C++11
public:
void set(T* raw) { ptr_.reset(raw); }
T const& get() const { return *ptr_; }
};
Registry<int> r1; r1.set(42);
Registry<int*> r2; r2.set(new int{7}); // ownership transferredVariable Template Specialization (C++14)
Variable templates introduced in C++14 support both full and partial specialization.
// C++14
template<typename T>
constexpr bool is_trivially_relocatable = std::is_trivially_copyable_v<T>; // C++17 _v helper
// Partial: pointers are always trivially relocatable
template<typename T>
constexpr bool is_trivially_relocatable<T*> = true;
// Full: a known-safe type
struct Vec2 { float x, y; };
template<>
constexpr bool is_trivially_relocatable<Vec2> = true;Specializing Standard Library Traits
The standard permits user-defined specializations of std::hash, std::less (by policy), and std::numeric_limits for user types. Specializing most other std:: templates is undefined behaviour.
#include <functional>
#include <unordered_map>
struct EntityId {
uint32_t world;
uint32_t index;
bool operator==(EntityId const&) const = default; // C++20
};
template<>
struct std::hash<EntityId> {
std::size_t operator()(EntityId const& id) const noexcept {
// Combine two 32-bit values into a single hash.
// Bit-mixing avoids collisions when world/index share low bits.
std::size_t h = std::hash<uint32_t>{}(id.world);
h ^= std::hash<uint32_t>{}(id.index) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h;
}
};
std::unordered_map<EntityId, std::string> names;
names[{0, 1}] = "player";Explicit Instantiation — Controlling Code Generation
Explicit instantiation separates the where of template code generation from the when of its use. Without it, every translation unit (TU) that uses vector<MyType> emits and discards its own copy of every vector<MyType> member function.
// mytypes.h
#include <vector>
struct Particle { float x, y, z; float mass; };
// Suppress implicit instantiation in every TU that includes this header.
// C++11 extern template
extern template class std::vector<Particle>;
// physics.cpp — the one TU that emits the full instantiation
#include "mytypes.h"
template class std::vector<Particle>; // explicit instantiation definition
// result: smaller .o files and faster link times for large projectsThe extern template declaration (C++11) must precede any use of vector<Particle> that would trigger implicit instantiation; the explicit instantiation definition must appear in exactly one TU.
Best Practices
Prefer overloads over function template specializations. Function template specialization interacts poorly with overload resolution: a non-template overload always wins against a template (desired), but a function template specialization does not participate in overload resolution — it is selected after the primary template has already been chosen, bypassing any non-template overloads added later. The result is surprising call dispatch.
template<typename T> void log(T x) { /* primary */ }
template<> void log(int x) { /* specialization — silent if an
overload of log(int) is added
later in another header */ }
// CORRECT: plain overload
void log(int x) { /* always preferred over the template */ }Use if constexpr (C++17) for small per-type branching inside a single function body. Reserve specialization for cases where the interface, data layout, or class hierarchy genuinely differs between types.
// C++17
template<typename T>
void serialize(std::ostream& out, T const& val) {
if constexpr (std::is_arithmetic_v<T>) { // C++17
out.write(reinterpret_cast<char const*>(&val), sizeof(T));
} else if constexpr (requires { val.serialize(out); }) { // C++20 requires-expression
val.serialize(out);
} else {
static_assert(sizeof(T) == 0, "T is not serializable");
}
}Declare full specializations in the header, define them in a single .cpp. If a full specialization is defined in a header included by multiple TUs, you get an ODR violation. Forward-declare with extern or define inline/constexpr.
Common Pitfalls
Partial specialization on function templates is illegal. You can fully specialize a function template, but not partially. Workaround: delegate to a class template helper.
// Won't compile — partial function template specialization is not allowed
template<typename T>
void process(T* x) { } // this is an overload, not a specialization
// Correct pattern: partial specialization lives on a struct
template<typename T, bool IsPtr>
struct ProcessImpl { static void run(T x) { /* value */ } };
template<typename T>
struct ProcessImpl<T, true> { static void run(T x) { /* pointer */ } };
template<typename T>
void process(T x) { ProcessImpl<T, std::is_pointer_v<T>>::run(x); } // C++17Specialization must be more specialized, not just different. If a partial specialization is not strictly more constrained than the primary for any argument pattern, you get an error at instantiation when both match equally.
Explicit instantiation of a specialization requires the specialization to have been declared first. If the compiler sees template class X<int> before template<> class X<int>, it instantiates the primary, not the specialization.
static_assert(false) in an unselected if constexpr branch still fires (pre-C++23). Use a type-dependent expression instead: static_assert(sizeof(T) == 0, ...). C++23 introduces static_assert(false) being valid in a discarded branch of if constexpr.
See Also
- CRTP — partial specialization enables the Curiously Recurring Template Pattern
- Type Erasure — specialization used to dispatch to type-specific storage
- if constexpr — C++17 alternative for compile-time branching inside templates
- Templates — primary templates, template parameters, and instantiation