Skip to content
C++
Language
since C++98
Intermediate

Partial Specialization

Provide an alternative class template implementation for a constrained subset of template arguments, leaving remaining parameters free.

Partial Specializationsince C++98

A class template mechanism that provides an alternative definition triggered by a constrained pattern of template arguments, while one or more template parameters remain free.

Overview

Partial specialization occupies the space between a primary template—fully generic—and an explicit full specialization, where every argument is fixed. You constrain some arguments by pattern or value while others remain free, giving the compiler a more specific match to prefer during instantiation.

Only class templates support partial specialization. Function templates cannot be partially specialized; the language intentionally omits this, and the equivalent mechanism is overloading. Any attempt to write template<typename T> void f<T*>() is ill-formed. Since C++14, variable templates can also carry partial specializations.

When a template is instantiated, the compiler collects every partial specialization whose argument pattern unifies with the supplied arguments, then applies partial ordering to select the most specialized candidate. If two candidates tie, the program is ill-formed at the point of instantiation—the error appears when you write S<X, Y>, not when you define the specializations. If no partial specialization matches, the primary template is used.

Syntax

cpp
// Primary template — two free parameters
template <typename T, int N>
struct Buffer { /* general */ };

// Partial specialization: N fixed to 0, T still free
template <typename T>
struct Buffer<T, 0> { /* zero-capacity case */ };

// Partial specialization: T constrained to any pointer type, N still free
template <typename T, int N>
struct Buffer<T*, N> { /* pointer-element optimization */ };

// Partial specialization: T a pointer AND N == 0 — most specific of the three
template <typename T>
struct Buffer<T*, 0> { /* zero-capacity pointer case */ };

The template <...> list names only the remaining free parameters needed to describe the specialization. The argument list after the class name is the pattern tested against each instantiation.

Examples

Type trait: is_pointer

The most common introductory example is also genuinely useful:

cpp
template <typename T>
struct is_pointer {
    static constexpr bool value = false;  // C++11: constexpr
};

template <typename T>
struct is_pointer<T*> {
    static constexpr bool value = true;
};

template <typename T>
struct is_pointer<T* const> {
    static constexpr bool value = true;
};

static_assert(!is_pointer<int>::value);
static_assert( is_pointer<int*>::value);
static_assert( is_pointer<double* const>::value);

Since C++11, <type_traits> ships std::is_pointer<T> using the same technique. Since C++17, std::is_pointer_v<T> aliases std::is_pointer<T>::value via a variable template partial specialization.

Thin-template pointer optimization

Storing pointers of every type in separate instantiations causes code bloat. A partial specialization can delegate to a single void* implementation:

cpp
template <typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(T v) { data_.push_back(std::move(v)); }
    T    pop()     { T v = std::move(data_.back()); data_.pop_back(); return v; }
    bool empty() const { return data_.empty(); }
};

// Partial specialization: all pointer instantiations share one machine-code body
template <typename T>
class Stack<T*> {
    Stack<void*> impl_;
public:
    void  push(T* p)  { impl_.push(static_cast<void*>(p)); }
    T*    pop()       { return static_cast<T*>(impl_.pop()); }
    bool  empty() const { return impl_.empty(); }
};

TMP recursion termination

Recursive template metaprogramming needs a base case that halts at compile time. Partial specialization is the canonical way to provide it:

cpp
// Compile-time index sequence printer for tuples
template <std::size_t I, std::size_t N, typename... Ts>
struct TuplePrinter {
    static void print(std::ostream& os, const std::tuple<Ts...>& t) {
        os << std::get<I>(t);
        if constexpr (I + 1 < N) os << ", ";  // C++17: if constexpr
        TuplePrinter<I + 1, N, Ts...>::print(os, t);
    }
};

// Partial specialization: I == N terminates the recursion
template <std::size_t N, typename... Ts>
struct TuplePrinter<N, N, Ts...> {
    static void print(std::ostream&, const std::tuple<Ts...>&) {}
};

template <typename... Ts>
void print_tuple(std::ostream& os, const std::tuple<Ts...>& t) {
    os << '(';
    TuplePrinter<0, sizeof...(Ts), Ts...>::print(os, t);
    os << ')';
}

Variable template partial specialization (C++14)

Since C++14, variable templates accept partial specializations in the same way as class templates:

cpp
// Primary: assume not integral
template <typename T>
constexpr bool is_integral_v = false;  // C++14

// Full specializations for concrete types
template <> constexpr bool is_integral_v<int>      = true;
template <> constexpr bool is_integral_v<long>     = true;
template <> constexpr bool is_integral_v<long long> = true;

// Partial specialization: strip const, then re-evaluate
template <typename T>
constexpr bool is_integral_v<const T> = is_integral_v<T>;

// Partial specialization: strip volatile
template <typename T>
constexpr bool is_integral_v<volatile T> = is_integral_v<T>;

static_assert(is_integral_v<const long>);

The standard library generalizes this pattern in <type_traits> since C++17.

Matching on qualifiers and reference categories

Argument patterns can match cv-qualifiers, references, and combinations:

cpp
template <typename T>
struct Unwrap {
    using type = T;
};

template <typename T>
struct Unwrap<T&> {           // lvalue reference — C++11 category
    using type = T;
};

template <typename T>
struct Unwrap<T&&> {          // rvalue reference — C++11
    using type = T;
};

template <typename T>
struct Unwrap<const T> {
    using type = T;
};

// Helper alias — C++14
template <typename T>
using Unwrap_t = typename Unwrap<T>::type;

static_assert(std::is_same_v<Unwrap_t<const int&>, int>);  // chains: const→& strip

Best Practices

Reach for if constexpr or concepts before adding a specialization. Since C++17, if constexpr eliminates the need for many structural specializations inside member functions. Since C++20, concepts constrain templates declaratively without the overhead of a parallel class hierarchy.

Make the primary template self-documenting about which specializations exist. If the primary has no sensible general behavior, express that as a static_assert:

cpp
template <typename T>
struct Codec {
    static_assert(sizeof(T) == 0,
        "No Codec<T> defined for this type. "
        "Provide a specialization or include the relevant header.");
};

This produces a legible error rather than a linker failure.

Prefer a single partial specialization over multiple full specializations when the pattern is uniform. A single template<typename T> struct Foo<T*> covers every pointer type; enumerating int*, double*, etc. individually is both verbose and incomplete.

Common Pitfalls

Function templates cannot be partially specialized. Delegate to a class template instead:

cpp
// WRONG — ill-formed
template <typename T>
void process<T*>(T* p);   // error: partial specialization of a function template

// RIGHT — overload
template <typename T> void process(T  v) { /* generic */ }
template <typename T> void process(T* p) { /* pointer */ }

// RIGHT — when overloading isn't sufficient (e.g., you need to specialize a
//          behavior trait), delegate:
template <typename T>
struct ProcessImpl { static void run(T v) { /* generic */ } };

template <typename T>
struct ProcessImpl<T*> { static void run(T* p) { /* pointer */ } };

template <typename T>
void process(T v) { ProcessImpl<T>::run(v); }

Ambiguous partial specializations are a hard error at instantiation, not at definition. Two equally-specific specializations appear to coexist peacefully until you trigger the ambiguous instantiation:

cpp
template <typename T, typename U> struct S {};
template <typename T>             struct S<T,  T>  {};   // A: both same type
template <typename T>             struct S<T*, T*> {};   // B: both pointer to same type

S<int,  int>  a;   // OK — only A matches
S<int*, int*> b;   // OK — B is more specialized than A, so B wins

Adding a third specialization like template<typename T> struct S<T, T*> {} can create genuine ambiguity for certain argument combinations. Test edge cases at the point of definition with static_assert or explicit instantiation declarations.

The primary template must be declared before any specialization. Specializations referencing an undeclared primary are ill-formed. In practice, always place the primary and all its specializations in the same header, in order.

Template template parameters are deduced more restrictively. When a partial specialization argument involves a template template parameter, deduction applies different rules than for plain type parameters—particularly around default template arguments of the inner template (this tightened in C++17).

See Also

  • Template Specialization — explicit (full) specialization and the overall specialization model
  • Class Template — primary template syntax, member definitions, and instantiation
  • SFINAE — substitution-failure-based selection, complementary to specialization for function templates