Explicit and Partial Specialization
Explicit and Partial Specialization
Section titled “Explicit and Partial Specialization”Specialization allows you to provide alternative implementations for specific sets of template Arguments. Full specialization replaces the primary template entirely for a specific type, while partial specialization provides a pattern-matched alternative that the compiler selects using Partial ordering rules.
Full Specialization
Section titled “Full Specialization”Full (explicit) specialization provides a completely separate definition for a specific set of Template arguments [N4950 S13.7.5]. The general template is called the primary template.
#include <iostream>#include <type_traits>#include <string>
// Primary template [N4950 S13.7.5]template <typename T, typename U>struct is_same { static constexpr bool value = false;};
// Full specialization for T == Utemplate <typename T>struct is_same<T, T> { static constexpr bool value = true;};
// The standard library version is std::is_same_v [N4950 S20.15.4.3].
template <typename T, typename U>inline constexpr bool is_same_v = is_same<T, U>::value;
int main() { static_assert(!is_same_v<int, double>); static_assert(is_same_v<int, int>); static_assert(is_same_v<const int, const int>); static_assert(!is_same_v<int, const int>); static_assert(is_same_v<std::string, std::string>);
std::cout << std::boolalpha; std::cout << is_same_v<int, double> << "\n"; // false std::cout << is_same_v<int, int> << "\n"; // true}:::caution Full specializations are not templates themselves --- they are concrete definitions. They must be declared in the same namespace as the primary template. If you fully specialize a Function template, you must specialize every overload that participates in overload resolution. :::
Full Specialization of Function Templates
Section titled “Full Specialization of Function Templates”Function templates can be fully specialized, but this is rarely recommended because overloading provides a better solution:
#include <iostream>#include <cstring>
// Primary function templatetemplate <typename T>T clamp(T value, T lo, T hi) { return value < lo ? lo : (value > hi ? hi : value);}
// Full specialization for const char* -- but overloading is preferred!template <>const char* clamp<const char*>(const char* value, const char* lo, const char* hi) { if (std::strcmp(value, lo) < 0) return lo; if (std::strcmp(value, hi) > 0) return hi; return value;}
int main() { std::cout << clamp(5, 0, 10) << "\n"; // 5 std::cout << clamp(15, 0, 10) << "\n"; // 10 std::cout << clamp("banana", "apple", "cherry") << "\n"; // banana}Why overloading is preferred over function specialization: Overloads participate in overload Resolution at the same level, while specializations do not. A full specialization of a function Template is only considered if the primary template is already the best match, which can lead to Surprising behavior. Per [N4950 S13.7.5/4], a full function template specialization is selected only After overload resolution has already chosen the primary template. This means that a non-template Overload that is a better match will always be preferred over a specialization.
Partial Specialization
Section titled “Partial Specialization”Partial specialization allows you to specialize for a subset of possible template arguments. The primary template still exists for arguments that don”t match any partial specialization. The Compiler selects the most specialized version using partial ordering rules [N4950 S13.7.5.5].
#include <iostream>#include <type_traits>#include <string>#include <vector>
// Primary templatetemplate <typename T>struct remove_cv { using type = T;};
// Partial specialization: match const Ttemplate <typename T>struct remove_cv<const T> { using type = T;};
// Partial specialization: match volatile Ttemplate <typename T>struct remove_cv<volatile T> { using type = T;};
// Partial specialization: match const volatile Ttemplate <typename T>struct remove_cv<const volatile T> { using type = T;};
template <typename T>using remove_cv_t = typename remove_cv<T>::type;
// More partial specialization examples:
// Primary template for is_pointertemplate <typename T>struct is_pointer : std::false_type {};
// Partial specialization: match T*template <typename T>struct is_pointer<T*> : std::true_type {};
template <typename T>inline constexpr bool is_pointer_v = is_pointer<T>::value;
// Partial specialization for remove_referencetemplate <typename T>struct remove_reference { using type = T; };
template <typename T>struct remove_reference<T&> { using type = T; };
template <typename T>struct remove_reference<T&&> { using type = T; };
template <typename T>using remove_reference_t = typename remove_reference<T>::type;
int main() { static_assert(std::is_same_v<remove_cv_t<const int>, int>); static_assert(std::is_same_v<remove_cv_t<volatile double>, double>); static_assert(std::is_same_v<remove_cv_t<const volatile char>, char>); static_assert(std::is_same_v<remove_cv_t<int>, int>);
static_assert(is_pointer_v<int*>); static_assert(is_pointer_v<const int*>); static_assert(!is_pointer_v<int>); static_assert(!is_pointer_v<int&>);
static_assert(std::is_same_v<remove_reference_t<int&>, int>); static_assert(std::is_same_v<remove_reference_t<int&&>, int>); static_assert(std::is_same_v<remove_reference_t<int>, int>);
std::cout << std::boolalpha; std::cout << is_pointer_v<int*> << "\n"; // true std::cout << is_pointer_v<int> << "\n"; // false}Partial Ordering Rules
Section titled “Partial Ordering Rules”When multiple partial specializations match, the compiler uses partial ordering to select the Most specialized one [N4950 S13.7.5.5]. Informally, specialization is more specialized than If every type accepted by is also accepted by But not vice versa.
#include <iostream>#include <type_traits>
template <typename T>struct DebugName { static const char* name() { return "primary"; }};
template <typename T>struct DebugName<T*> { static const char* name() { return "pointer"; }};
template <typename T>struct DebugName<T* const> { static const char* name() { return "const pointer"; }};
int main() { std::cout << DebugName<int>::name() << "\n"; // "primary" std::cout << DebugName<int*>::name() << "\n"; // "pointer" std::cout << DebugName<int* const>::name() << "\n"; // "const pointer" // For int* const, both T* and T* const match, but // T* const is more specialized (a subset of T*).}Formal Partial Ordering Algorithm
Section titled “Formal Partial Ordering Algorithm”The partial ordering algorithm [N4950 S13.7.5.5/2] works by synthetic substitution:
Given two partial specializations and that both match a given set of template arguments, the compiler attempts to determine which is “more specialized.”
To test whether is at least as specialized as The compiler replaces each template parameter of with a unique synthetic type and checks whether the resulting pattern matches . If it does, is at least as specialized as .
The compiler then performs the same test in the other direction: replace each template parameter of with a unique synthetic type and check whether it matches .
If matches but does not match Then is more specialized. If both match each other, they are ambiguous. If neither matches the other, neither is more specialized.
Proof that T* const is more specialized than T*. Replace the T in T* const with a unique Type . The result is U_{unique}* const. Now check: does this match the pattern T*? Yes, with T = U_{unique} const. Conversely, replace the T in T* with a unique type . The result is V_{unique}*. Does this match the pattern T* const? No, because V_{unique}* is not const-qualified. Therefore T* const is strictly more specialized than T*.
Ordering Example: Pointers vs Arrays
Section titled “Ordering Example: Pointers vs Arrays”#include <iostream>#include <type_traits>
template <typename T>struct TypeInfo { static const char* name() { return "unknown"; }; };
template <typename T>struct TypeInfo<T*> { static const char* name() { return "pointer"; }; };
template <typename T, std::size_t N>struct TypeInfo<T[N]> { static const char* name() { return "array"; }; };
int main() { std::cout << TypeInfo<int>::name() << "\n"; // "unknown" std::cout << TypeInfo<int*>::name() << "\n"; // "pointer" std::cout << TypeInfo<int[10]>::name() << "\n"; // "array" // Note: int[10] does NOT decay to int* for partial matching. // T[N] is more specialized than T* for array types.}Proof That Ambiguous Specializations Are Ill-Formed
Section titled “Proof That Ambiguous Specializations Are Ill-Formed”When two partial specializations are equally specialized, the program is ill-formed [N4950 S13.7.5.5/1]. The reasoning is as follows: partial ordering is a strict weak ordering on the set Of matching specializations. If neither nor holds (where means “at least As specialized as”), then and are incomparable under the ordering. Since the ordering Must produce a unique maximum element, incomparable elements represent an ambiguity, and the Standard requires a diagnostic.
template <typename T>struct Ambig {};
// Both specializations match Ambig<const int*> -- ambiguous!template <typename T>struct Ambig<const T*> {}; // matches: T = int
template <typename T>struct Ambig<T* const> {}; // matches: T = const int
// Ambig<const int*> ai; // ERROR: ambiguous partial specializationProof of ambiguity. Let = const T* and = T* const. Replace T in with a unique Type : we get const U*. Does this match (T* const)? Yes, with T = \mathrm{const U. Now Replace T in with a unique type : we get V* const. Does this match (const T*)? Yes, With T = V \mathrm{ const. Since matches and matches Neither is strictly More specialized. The program is ill-formed.
The fix is to provide a disambiguating specialization that is strictly more specialized than both:
template <typename T>struct Ambig<const T* const> {}; // Matches const pointers, strictly more specialized// Now Ambig<const int*> still ambiguous between first two.// The real fix is to avoid overlapping patterns entirely.Partial Specialization with SFINAE
Section titled “Partial Specialization with SFINAE”Partial specializations can use SFINAE via constraints (C++20 requires) or std::enable_if to Select implementations based on type properties:
#include <iostream>#include <type_traits>#include <string>#include <vector>
// Primary templatetemplate <typename T, typename = void>struct element_type { using type = T;};
// Partial specialization for containers with ::value_typetemplate <typename T>struct element_type<T, std::void_t<typename T::value_type>> { using type = typename T::value_type;};
template <typename T>using element_type_t = typename element_type<T>::type;
// C++20 version using conceptstemplate <typename T>struct element_type_cxx20 { using type = T;};
template <typename T> requires requires { typename T::value_type; }struct element_type_cxx20<T> { using type = typename T::value_type;};
int main() { static_assert(std::is_same_v<element_type_t<int>, int>); static_assert(std::is_same_v<element_type_t<std::vector<int>>, int>); static_assert(std::is_same_v<element_type_t<std::string>, char>); static_assert(std::is_same_v<element_type_t<std::vector<std::string>>, std::string>);
std::cout << "element_type works for primitives and containers\n";}SFINAE in Specialization Contexts
Section titled “SFINAE in Specialization Contexts”SFINAE applies differently in partial specializations than in function template overload resolution. In a partial specialization, the SFINAE check occurs when the compiler tries to match the Specialization pattern against the given template arguments. If the substitution of arguments into The specialization pattern fails, the specialization is not considered --- it is not an Error:
#include <iostream>#include <type_traits>
// Primary templatetemplate <typename T, typename = void>struct has_size : std::false_type {};
// Partial specialization: enabled only if T has a .size() member functiontemplate <typename T>struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};
struct Sized { std::size_t size() const { return 0; } };struct NotSized {};
int main() { static_assert(has_size<Sized>::value); static_assert(!has_size<NotSized>::value); static_assert(!has_size<int>::value); std::cout << std::boolalpha; std::cout << has_size<Sized>::value << "\n"; // true std::cout << has_size<NotSized>::value << "\n"; // false}With C++20 constraints, the same pattern is cleaner and produces better error messages:
#include <iostream>#include <type_traits>#include <vector>
template <typename T> requires requires(const T& t) { t.size(); }std::size_t get_size(const T& obj) { return obj.size();}
template <typename T>std::size_t get_size(const T&) { return 0;}
struct Custom { std::size_t size() const { return 42; } };
int main() { std::cout << get_size(std::vector<int>{1, 2, 3}) << "\n"; // 3 std::cout << get_size(Custom{}) << "\n"; // 42 std::cout << get_size(42) << "\n"; // 0}Template Template Parameters
Section titled “Template Template Parameters”A template template parameter is a template parameter that is itself a template. This enables Specialization on the “shape” of a type:
#include <iostream>#include <vector>#include <list>#include <deque>
// Primary template: accepts any container of Ttemplate <typename T, template <typename, typename> class Container>struct ContainerInfo { static void print() { std::cout << "generic container\n"; }};
// Partial specialization for std::vectortemplate <typename T, typename Alloc>struct ContainerInfo<T, std::vector> { static void print() { std::cout << "std::vector (contiguous, random-access)\n"; }};
// Partial specialization for std::listtemplate <typename T, typename Alloc>struct ContainerInfo<T, std::list> { static void print() { std::cout << "std::list (doubly-linked, bidirectional)\n"; }};
int main() { ContainerInfo<int, std::vector>::print(); // std::vector (contiguous, ...) ContainerInfo<int, std::list>::print(); // std::list (doubly-linked, ...) ContainerInfo<int, std::deque>::print(); // generic container}Template Template Parameter Matching (C++17)
Section titled “Template Template Parameter Matching (C++17)”C++17 relaxed the rules for template template parameter matching [N4950 S13.3.3]. Previously, a Template template parameter had to match the exact parameter list of the template argument (including default arguments). C++17 allows a template template parameter with fewer parameters than The template argument, as long as the parameters are deducible:
#include <iostream>#include <vector>
// C++17: OK even though std::vector has two template parameters (T, Allocator)// The template template parameter Container only needs one (T).template <typename T, template <typename> class Container>class Adapter { Container<T> data_;public: void add(const T& v) { data_.push_back(v); } std::size_t size() const { return data_.size(); }};
int main() { Adapter<int, std::vector> a; a.add(1); a.add(2); std::cout << a.size() << "\n"; // 2}Variadic Template Specialization
Section titled “Variadic Template Specialization”Variadic templates can be partially specialized to handle specific pack patterns:
#include <iostream>
// Primary template: recursive casetemplate <typename... Ts>struct Count { static constexpr std::size_t value = 1 + Count<Ts...>::value;};
// Partial specialization: empty pack (base case)template <>struct Count<> { static constexpr std::size_t value = 0;};
// Partial specialization: single typetemplate <typename T>struct Count<T> { static constexpr std::size_t value = 1;};
// Specialization pattern: first type + resttemplate <typename First, typename... Rest>struct Front { using type = First;};
template <typename First, typename... Rest>using Front_t = typename Front<First, Rest...>::type;
int main() { static_assert(Count<int, double, char>::value == 3); static_assert(Count<>::value == 0); static_assert(Count<int>::value == 1);
static_assert(std::is_same_v<Front_t<int, double, char>, int>); static_assert(std::is_same_v<Front_t<char>, char>);
std::cout << Count<int, double, char, float>::value << "\n"; // 4}Variadic Specialization Patterns
Section titled “Variadic Specialization Patterns”Variadic partial specialization enables several important patterns. Here are the most common ones:
Pattern 1: Head/tail decomposition. Peel off the first element of a pack and recurse on the Rest. This is the foundation of most compile-time list algorithms:
#include <iostream>#include <type_traits>
// Head of a type listtemplate <typename... Ts>struct Head;
template <typename First, typename... Rest>struct Head<First, Rest...> { using type = First;};
// Tail of a type listtemplate <typename... Ts>struct Tail;
template <typename First, typename... Rest>struct Tail<First, Rest...> { using type = Tail<Rest...>;};
template <typename Last>struct Tail<Last> { using type = Tail<>;};
template <>struct Tail<> {};
int main() { static_assert(std::is_same_v<Head<int, double, char>::type, int>); std::cout << "head works\n";}Pattern 2: Filter a type list by predicate.
#include <iostream>#include <type_traits>#include <vector>
// Primary: empty listtemplate <template <typename> class Pred, typename... Ts>struct Filter;
// Base case: empty listtemplate <template <typename> class Pred>struct Filter<Pred> { using type = std::tuple<>;};
// Recursive case: head satisfies predicatetemplate <template <typename> class Pred, typename Head, typename... Tail> requires Pred<Head>::valuestruct Filter<Pred, Head, Tail...> { using type = decltype( std::tuple_cat( std::declval<std::tuple<Head>>(), std::declval<typename Filter<Pred, Tail...>::type>() ));};
// Recursive case: head does not satisfy predicatetemplate <template <typename> class Pred, typename Head, typename... Tail> requires (!Pred<Head>::value)struct Filter<Pred, Head, Tail...> { using type = typename Filter<Pred, Tail...>::type;};
template <template <typename> class Pred, typename... Ts>using Filter_t = typename Filter<Pred, Ts...>::type;
int main() { using Types = int, double, float, char; using Integers = Filter_t<std::is_integral, int, double, float, char>; // Integers is std::tuple<int, char> static_assert(std::is_same_v<Integers, std::tuple<int, char>>); std::cout << "filter works\n";}Pattern 3: Concatenation of type lists.
#include <iostream>#include <type_traits>
template <typename List1, typename List2>struct Concat;
template <typename... T1s, typename... T2s>struct Concat<std::tuple<T1s...>, std::tuple<T2s...>> { using type = std::tuple<T1s..., T2s...>;};
template <typename L1, typename L2>using Concat_t = typename Concat<L1, L2>::type;
int main() { using A = std::tuple<int, double>; using B = std::tuple<char, float>; using C = Concat_t<A, B>; static_assert(std::is_same_v<C, std::tuple<int, double, char, float>>); std::cout << "concat works\n";}Variadic Type Traits with Specialization
Section titled “Variadic Type Traits with Specialization”#include <iostream>#include <type_traits>
// All-of trait using recursive specializationtemplate <typename... Conds>struct AllOf : std::true_type {};
template <typename First, typename... Rest>struct AllOf<First, Rest...> : std::conditional_t<bool(First::value), AllOf<Rest...>, std::false_type> {};
template <bool... Bools>inline constexpr bool all_of_v = AllOf<std::bool_constant<Bools>...>::value;
int main() { static_assert(all_of_v<true, true, true>); static_assert(!all_of_v<true, false, true>); static_assert(all_of_v<>); // empty: true std::cout << "all_of_v works\n";}Specialization of Member Templates
Section titled “Specialization of Member Templates”Member templates (template members of a class template) can be fully or partially specialized Independently of the enclosing class template. This is useful for providing type-specific Implementations of individual member functions:
#include <iostream>#include <string>
template <typename T>class Serializer {public: std::string serialize() const;
template <typename U> U convert() const;
T value_;};
// Full specialization of a member function for T = inttemplate <>std::string Serializer<int>::serialize() const { return std::to_string(value_);}
// Full specialization of a member template for T = int, U = doubletemplate <>template <>double Serializer<int>::convert<double>() const { return static_cast<double>(value_);}
// Generic member definitionstemplate <typename T>std::string Serializer<T>::serialize() const { return "generic serialization";}
template <typename T>template <typename U>U Serializer<T>::convert() const { return static_cast<U>(value_);}
int main() { Serializer<int> si{42}; std::cout << si.serialize() << "\n"; // "42" (specialized) std::cout << si.convert<double>() << "\n"; // 42.0 (specialized)
Serializer<double> sd{3.14}; std::cout << sd.serialize() << "\n"; // "generic serialization" std::cout << sd.convert<int>() << "\n"; // 3 (generic)}:::caution You cannot partially specialize a member template without partially specializing the Enclosing class template. Member templates can only be fully specialized. If you need partial Specialization of a member, you must partially specialize the entire class. :::
Common Errors with Ambiguity
Section titled “Common Errors with Ambiguity”Ambiguous Partial Specializations
Section titled “Ambiguous Partial Specializations”When two partial specializations are equally specialized, the program is ill-formed:
template <typename T>struct Ambig {};
// Both specializations match Ambig<const int*> -- ambiguous!template <typename T>struct Ambig<const T*> {}; // matches: T = int
template <typename T>struct Ambig<T* const> {}; // matches: T = const int
// Ambig<const int*> ai; // ERROR: ambiguous partial specialization
// Fix: provide a specialization that is strictly more specializedtemplate <typename T>struct Ambig<const T* const> {};// Now Ambig<const int*> still ambiguous between first two.// The real fix is to reorder: put the more specific one first// and ensure no overlap, or add a disambiguating specialization.Full vs Partial Specialization Ordering
Section titled “Full vs Partial Specialization Ordering”Full specializations always take precedence over partial specializations, regardless of declaration Order:
template <typename T>struct Prioritized { static constexpr int value = 0; };
template <typename T>struct Prioritized<T*> { static constexpr int value = 1; }; // partial
template <>struct Prioritized<int*> { static constexpr int value = 2; }; // full
int main() { static_assert(Prioritized<int*>::value == 2); // full specialization wins static_assert(Prioritized<double*>::value == 1); // partial specialization static_assert(Prioritized<int>::value == 0); // primary template}Specialization and Declaration Order
Section titled “Specialization and Declaration Order”Partial specializations must be declared before they are used, but the order of partial Specializations relative to each other does not matter for selection --- the compiler considers all Visible partial specializations and applies the partial ordering rules:
#include <iostream>
template <typename T>struct S { static constexpr int val = 0; };
// Order does not matter: compiler picks the most specialized.template <typename T>struct S<T*> { static constexpr int val = 1; };
template <typename T>struct S<T&> { static constexpr int val = 2; };
int main() { static_assert(S<int*>::val == 1); static_assert(S<int&>::val == 2); static_assert(S<int>::val == 0); std::cout << "ordering works\n";}Common Pitfalls
Section titled “Common Pitfalls”Partial specializations of function templates are not allowed. You can only partially specialize class templates and variable templates. For functions, use overloading instead. This is a fundamental asymmetry in the language [N4950 S13.7.5/5].
Specialization must be visible at the point of use. If you specialize a template in a different translation unit, the specialization may not be used. Prefer header-only templates or explicit instantiation. The compiler selects specializations from among those that are visible at the point of instantiation.
Specialization does not inherit from the primary template. Each specialization is a completely independent definition. If you want shared behavior, use a base class or CRTP. Members defined in the primary template are not automatically available in specializations.
std::enable_ifin partial specializations. Usingstd::enable_ifas a template argument is the SFINAE-compatible way to conditionally specialize. C++20requiresclauses are preferred because they produce better error messages and compose more .Ambiguity is a hard error. If two partial specializations are equally specialized, the compiler does not pick one --- it emits an error. Always test with edge cases that exercise the boundaries of your specialization patterns.
Default template arguments and specialization interaction. Default arguments on the primary template do not affect which partial specialization is selected. The partial specialization pattern must match the actual arguments (including defaults) for selection to occur.
Member template specialization limitations. Member templates of class templates can only be fully specialized, not partially specialized. To partially specialize a member, partially specialize the entire enclosing class template. This often leads to code duplication when only one member needs specialization.
See Also
Section titled “See Also”- Template Instantiation, Monomorphization, and Code Bloat
- Argument Deduction (Class and Function)
- Dependent Names and Two-Phase Lookup
- Type Traits and Static Reflection Patterns
Summary
Section titled “Summary”This topic covers the core concepts of explicit and partial specialization, including underlying theory, practical implementation, and key applications.
Key concepts include:
- Big O notation and complexity analysis
- searching algorithms (binary, linear)
- sorting algorithms (bubble, merge, quick)
- graph algorithms (Dijkstra, BFS, DFS)
- dynamic programming
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.