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).

Partial Specialization for Pointer and Reference Types

Pointer and reference specializations are among the most common uses of partial specialization in production C++. The pattern appears throughout <memory>, <functional>, and allocator infrastructure.

cpp
// Primary template
template <typename T>
struct Storage {
    T value;
    void store(T v) { value = std::move(v); }
    T    load()     { return value; }
};

// Partial specialization for pointer types — C++11
template <typename T>
struct Storage<T*> {
    T* ptr = nullptr;
    void store(T* p) { ptr = p; }          // ownership stays outside
    T*   load()      { return ptr; }
};

// Reference specialization (stores as pointer internally) — C++11
template <typename T>
struct Formatter<T&> {
    T* ref_;
    explicit Formatter(T& r) : ref_(&r) {}
    std::string format() const { return std::to_string(*ref_); }
};

A classic technique for minimizing code size is the void* type-erasing pointer pattern: one shared implementation holds void*, and the typed specialization casts around it.

cpp
// Type-erasing void* base — C++11
template <>
struct Storage<void*> {
    void* ptr = nullptr;
    void store(void* p) { ptr = p; }
    void* load()        { return ptr; }
};

// All T* specializations delegate to the void* base
template <typename T>
struct Storage<T*> {
    Storage<void*> impl_;
    void store(T* p)  { impl_.store(static_cast<void*>(p)); }
    T*   load()       { return static_cast<T*>(impl_.load()); }
};

This is precisely how std::pointer_traits and early Boost pointer containers avoid generating separate machine code for each pointer type. std::hash<T*> also relies on a pointer partial specialization to redirect hashing to the underlying address.

Specializing on Template Template Parameters

Template template parameters let you match on the outer template itself, independently of its argument. This enables type traits that ask "is this type an instance of std::vector?" rather than "is this exactly std::vector<int>?".

cpp
// Primary: not a recognized container wrapper — C++11
template <typename T>
struct Wrap {
    T value;
};

// Partial specialization: matches any Container<T> — C++11
template <template<typename> class Container, typename T>
struct Wrap<Container<T>> {
    Container<T> data;
    std::size_t size() const { return data.size(); }
};

Wrap<int>                  plain;   // primary
Wrap<std::vector<double>>  vec;     // specialization: Container=std::vector, T=double
Wrap<std::list<std::string>> lst;   // specialization: Container=std::list,   T=string

A more targeted application is an is_specialization_of trait that checks exact outer-template identity:

cpp
// is_specialization_of — C++11
template <typename T, template<typename...> class Primary>
struct is_specialization_of : std::false_type {};

template <template<typename...> class Primary, typename... Args>
struct is_specialization_of<Primary<Args...>, Primary> : std::true_type {};

// Helper variable template — C++17
template <typename T, template<typename...> class Primary>
inline constexpr bool is_specialization_of_v =
    is_specialization_of<T, Primary>::value;

static_assert( is_specialization_of_v<std::vector<int>,    std::vector>);
static_assert(!is_specialization_of_v<std::list<int>,      std::vector>);
static_assert( is_specialization_of_v<std::optional<bool>, std::optional>);

Note that template template parameter deduction became more consistent in C++17 (P0522): default template arguments of the inner template no longer block matching, so code that failed under C++14 compilers may now compile correctly.

Partial Specialization in Type Traits

The standard type traits in <type_traits> are almost entirely implemented as chains of partial specializations. Understanding the pattern lets you build your own traits that compose cleanly.

std::remove_pointer<T> strips one level of pointer indirection:

cpp
// std::remove_pointer — C++11 canonical implementation
template <typename T>         struct remove_pointer      { using type = T; };
template <typename T>         struct remove_pointer<T*>  { using type = T; };
template <typename T>         struct remove_pointer<T* const>    { using type = T; };
template <typename T>         struct remove_pointer<T* volatile> { using type = T; };
template <typename T>         struct remove_pointer<T* const volatile> { using type = T; };

template <typename T>
using remove_pointer_t = typename remove_pointer<T>::type;  // C++14 alias

static_assert(std::is_same_v<remove_pointer_t<int**>, int*>); // one level only

std::is_array<T> requires two specializations—one for unknown bounds, one for known bounds:

cpp
// std::is_array — C++11
template <typename T>           struct is_array       : std::false_type {};
template <typename T>           struct is_array<T[]>  : std::true_type  {};  // unknown bound
template <typename T, size_t N> struct is_array<T[N]> : std::true_type  {};  // known bound

static_assert( is_array<int[]>::value);
static_assert( is_array<int[42]>::value);
static_assert(!is_array<int*>::value);

std::remove_extent<T> follows the identical two-specialization pattern to strip the outermost array dimension:

cpp
// std::remove_extent — C++11
template <typename T>           struct remove_extent        { using type = T; };
template <typename T>           struct remove_extent<T[]>   { using type = T; };
template <typename T, size_t N> struct remove_extent<T[N]>  { using type = T; };

Complex transformations are built by chaining: std::decay<T> applies remove_reference, then remove_cv, then pointer-adds arrays and functions—each step delegating to the appropriate single-purpose trait. The composability is what makes this pattern powerful; each trait does exactly one thing and delegates the rest.

Partial Specialization with Non-Type Parameters

Non-type template parameters can appear in partial specialization argument patterns, allowing specialization on specific values as well as types. The specialization must be strictly more specialized than the primary—it cannot widen the constraint.

cpp
// Primary: fixed-size buffer for any T and any N — C++11
template <typename T, size_t N>
struct FixedBuffer {
    T   data[N];
    static constexpr size_t capacity = N;
    T&       operator[](size_t i)       { return data[i]; }
    const T& operator[](size_t i) const { return data[i]; }
};

// Partial specialization: zero-capacity buffer — no storage, no element access
template <typename T>
struct FixedBuffer<T, 0> {
    static constexpr size_t capacity = 0;
    // No data member — sizeof(FixedBuffer<T,0>) can be 1 (empty struct)
};

FixedBuffer<int, 8>  buf8;   // primary: 8 ints of storage
FixedBuffer<int, 0>  buf0;   // specialization: empty, still type-safe

Compile-time arithmetic is the other canonical use case, where non-type specialization provides the recursion base case:

cpp
// Compile-time factorial — C++11
template <int N>
struct Factorial {
    static constexpr long long value = N * Factorial<N - 1>::value;
};

template <>                          // full specialization for base case
struct Factorial<0> {
    static constexpr long long value = 1;
};

static_assert(Factorial<10>::value == 3628800);

When multiple non-type specializations are possible, partial ordering picks the most constrained: a specialization that fixes both a type and a value wins over one that fixes only the value.

Concepts as Constraints on Partial Specializations (C++20)

C++20 concepts can constrain partial specializations directly, giving a declarative alternative to SFINAE-based selection. A constrained specialization participates in overload resolution only when the constraint is satisfied, and a more-constrained specialization beats a less-constrained one.

cpp
// Primary template — C++20
template <typename T>
struct Storage {
    std::vector<std::byte> raw;
};

// Constrained partial specialization: only for integral types — C++20
template <typename T>
    requires std::integral<T>
struct Storage<T> {
    T value{};
    void store(T v) noexcept { value = v; }
    T    load()     const noexcept { return value; }
};

// Abbreviated concept syntax in template head — C++20
template <std::floating_point T>
struct Formatter<T> {
    std::string format(T v) const { return std::to_string(v); }
};

Partial ordering among constrained specializations follows the rule: a specialization with a strictly stronger constraint (i.e., its concept subsumes the other's) is preferred. Two specializations with unrelated constraints produce an ambiguity error at instantiation, just as unconstrained ambiguities do.

cpp
// Constrained and unconstrained specializations can coexist — C++20
template <typename T>
struct Tag { static constexpr std::string_view name = "generic"; };

template <std::integral T>
struct Tag<T> { static constexpr std::string_view name = "integral"; };

template <std::signed_integral T>   // std::signed_integral subsumes std::integral
struct Tag<T> { static constexpr std::string_view name = "signed integral"; };

static_assert(Tag<unsigned int>::name == "integral");       // integral wins over generic
static_assert(Tag<int>::name         == "signed integral"); // signed_integral wins over integral
static_assert(Tag<double>::name      == "generic");         // no constraint satisfied

Concept-constrained specializations are a clean replacement for many std::enable_if patterns and interact naturally with non-concept specializations in the same partial-specialization set.

Alias Templates and Partial Specialization

Alias templates cannot be partially specialized—attempting to do so is ill-formed. This is one of the most common surprises for developers migrating from class-template specialization patterns.

cpp
// WRONG — alias templates cannot be partially specialized
template <typename T>
using Ref = T&;

template <typename T>       // error: partial specialization of alias template
using Ref<T*> = T*&;        // ill-formed

The standard workaround is to wrap the logic in a class template, specialize that, and then expose an alias to its ::type member:

cpp
// Workaround: specialize a struct, alias the result — C++11
template <typename T>
struct RefImpl { using type = T&; };

template <typename T>
struct RefImpl<T*> { using type = T*; };   // pointers: no extra reference layer

template <typename T>
using Ref = typename RefImpl<T>::type;     // alias delegates to specializable struct

static_assert(std::is_same_v<Ref<int>,   int&>);
static_assert(std::is_same_v<Ref<int*>,  int*>);  // not int*&

This is the pattern behind every _t alias in <type_traits>: std::remove_const_t<T> is simply typename std::remove_const<T>::type, with the actual logic in the specializable class template.

In C++20, concepts often replace the need for dispatch-only specializations entirely. When the only reason for a specialization is to select a different behavior path, a concept-constrained if constexpr branch inside a single template is frequently cleaner:

cpp
// C++20: concept-gated branch instead of alias-workaround specialization
template <typename T>
auto make_ref(T& v) {
    if constexpr (std::is_pointer_v<T>)
        return v;       // pointer: return as-is
    else
        return std::ref(v);  // non-pointer: wrap in reference_wrapper
}

Reserve the struct-wrapping alias workaround for cases where you genuinely need a type-level mapping (i.e., the result must be a type, not a value), and concepts cannot express the distinction.

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