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

Class Template

A class template defines a parameterized family of classes, enabling type-safe generic data structures and algorithms without code duplication.

Class Templatesince C++98

A class template is a blueprint from which the compiler generates a family of related classes parameterized over types, values, or other templates.

Overview

A class template is not itself a class β€” it is a recipe. When you supply concrete template arguments (either explicitly or through deduction), the compiler stamps out a distinct class from that recipe. std::vector<int> and std::vector<std::string> are two completely separate types, both generated from the same std::vector<T> definition.

The motivating advantage is eliminating hand-written duplication: a single Stack<T> replaces separate IntStack, DoubleStack, and WidgetStack classes while preserving full type safety. Unlike runtime polymorphism, template instantiation resolves everything at compile time β€” no vtable, no indirection, no overhead.

Three kinds of template parameters exist:

  • Type parameters (typename T or class T) β€” accept any type, including built-ins and void.
  • Non-type parameters β€” integral constants, pointers, references, enumerators, nullptr_t; since C++20, also floating-point values and literal class types satisfying structural requirements.
  • Template template parameters β€” accept a template name itself as an argument, useful for policy-based designs.

Syntax

cpp
template <typename T, std::size_t N = 16>
class RingBuffer {
public:
    void push(const T& value);
    T    pop();
    bool empty() const noexcept;
    bool full()  const noexcept;

private:
    T           storage_[N];
    std::size_t head_  = 0;
    std::size_t tail_  = 0;
    std::size_t count_ = 0;
};

Member definitions that live outside the class body must repeat the full template header:

cpp
template <typename T, std::size_t N>
void RingBuffer<T, N>::push(const T& value) {
    if (full()) throw std::overflow_error{"ring buffer full"};
    storage_[tail_] = value;
    tail_ = (tail_ + 1) % N;
    ++count_;
}

template <typename T, std::size_t N>
bool RingBuffer<T, N>::full() const noexcept {
    return count_ == N;
}

The entire template β€” declaration and all member definitions β€” must be visible at every point of instantiation. In practice, keep everything in headers (or in a .tpp file #included at the bottom of the header). The sole exception is explicit instantiation: if you enumerate every needed specialization in a .cpp, you can keep definitions there β€” but this requires knowing all client types up front.

Default template arguments

Both type and non-type parameters accept defaults, evaluated left-to-right; later defaults may reference earlier parameters:

cpp
template <
    typename T,
    typename Allocator = std::allocator<T>  // T must be declared first
>
class MyVector { /* ... */ };

MyVector<int>       a;  // Allocator = std::allocator<int>
MyVector<int, Pool> b;  // custom allocator

Template template parameters

cpp
template <typename T, template <typename, typename> class Container = std::vector>
class Adapter {
    Container<T, std::allocator<T>> data_;
};

Adapter<int>             uses_vector;
Adapter<int, std::deque> uses_deque;

Examples

Policy-based design

Swapping strategies at compile time via template parameters achieves zero-cost configurability:

cpp
struct HeapPolicy {
    static void* allocate(std::size_t n) { return ::operator new(n); }
    static void  deallocate(void* p)     { ::operator delete(p); }
};

template <typename T, typename AllocPolicy = HeapPolicy>
class Buffer {
public:
    explicit Buffer(std::size_t cap)
        : data_{static_cast<T*>(AllocPolicy::allocate(cap * sizeof(T)))}
        , cap_{cap} {}
    ~Buffer() { AllocPolicy::deallocate(data_); }

    T&       operator[](std::size_t i)       { return data_[i]; }
    const T& operator[](std::size_t i) const { return data_[i]; }

private:
    T*          data_;
    std::size_t cap_;
};

Stack arenas, debug allocators, and pool allocators slot in without virtual dispatch or runtime overhead.

Class Template Argument Deduction β€” C++17

Before C++17, constructing std::pair required spelling out both types or delegating to std::make_pair. Since C++17, the compiler deduces template arguments from constructor call sites:

cpp
std::pair  p{42, 3.14};    // C++17 β€” deduces pair<int, double>
std::vector v{1, 2, 3};   // C++17 β€” deduces vector<int>

When implicit deduction is ambiguous or undesirable, provide an explicit deduction guide:

cpp
template <typename T>
struct Wrapper {
    T value;
    explicit Wrapper(T v) : value{std::move(v)} {}
};

// Deduction guide (C++17)
template <typename T>
Wrapper(T) -> Wrapper<T>;

Wrapper w{42};       // Wrapper<int>
Wrapper x{"hello"};  // Wrapper<const char*>

Constrained class templates β€” C++20

C++20 concepts replace manual SFINAE with readable, composable constraints:

cpp
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template <Numeric T>  // C++20 β€” constraint at declaration
class Statistics {
public:
    void insert(T value) { samples_.push_back(value); }

    T mean() const {
        if (samples_.empty()) return T{};
        return std::accumulate(samples_.begin(), samples_.end(), T{})
               / static_cast<T>(samples_.size());
    }

private:
    std::vector<T> samples_;
};

Statistics<int>    si;  // OK
Statistics<double> sd;  // OK
// Statistics<std::string> ss;  // error: constraint not satisfied

For multi-clause constraints, use a trailing requires clause:

cpp
template <typename T>
    requires Numeric<T> && std::is_copy_constructible_v<T>
class Statistics { /* ... */ };

Constraint violations are diagnosed at the point of use with actionable messages rather than deep template instantiation backtraces.

Partial Specialization

A partial specialization provides an alternative definition of the primary template for arguments that match a pattern β€” more constrained than the primary, but still parameterized by one or more remaining template variables.

The compiler compares all partial specializations against the requested arguments at instantiation time and selects the most specialized match. If no partial specialization matches, the primary template is used. If two partial specializations match equally, the result is an ambiguity error.

cpp
// Primary template: generic pair
template <typename T, typename U>      // C++98
class Pair {
public:
    T first;
    U second;
    void describe() const {
        std::cout << "generic Pair<T, U>\n";
    }
};

// Partial specialization: second type fixed to bool
// The specialization is "more constrained" β€” it applies only when U == bool.
template <typename T>                  // C++98 β€” one remaining parameter
class Pair<T, bool> {
public:
    T    first;
    bool second : 1;   // can use a bitfield here for compact storage

    void describe() const {
        std::cout << "Pair<T, bool> β€” compact boolean variant\n";
    }
    // May add bool-specific helpers unavailable in the generic version.
    void set_flag(bool v) { second = v; }
};

Pair<int, double>  a;  // primary template β€” Pair<int, double>
Pair<int, bool>    b;  // partial specialization β€” Pair<int, bool>
b.set_flag(true);      // method only available in the specialization

The specialization header template <typename T> still carries its free parameter; the pattern Pair<T, bool> fixes the second argument. This is the key distinction from a full specialization (template<>) which fixes all arguments and is therefore not parameterized at all.

A common application is providing different storage or behavior for pointer types:

cpp
// Primary: store by value
template <typename T>
class Box {
    T value_;
public:
    explicit Box(T v) : value_{std::move(v)} {}
    const T& get() const { return value_; }
};

// Partial specialization: store by owning pointer
template <typename T>
class Box<T*> {
    std::unique_ptr<T> ptr_;     // C++11
public:
    explicit Box(T* raw) : ptr_{raw} {}
    const T& get() const { return *ptr_; }
};

Box<int>   b1{42};           // stores int by value
Box<int*>  b2{new int{42}};  // stores via unique_ptr

The specialization must be strictly more constrained than the primary. A partial specialization that does not restrict the argument set any further than the primary is ill-formed. The compiler validates this: if both the primary and the specialization could match the same argument with equal specificity, an ambiguity error fires at instantiation.

CTAD β€” Class Template Argument Deduction (C++17)

Prior to C++17, every class template instantiation required spelling out the template arguments. std::make_pair, std::make_unique, and similar factory functions existed solely to work around this limitation. C++17 introduced Class Template Argument Deduction (CTAD), which lets the compiler infer template arguments directly from constructor arguments.

cpp
// C++17 β€” no explicit template arguments needed
std::vector  v{1, 2, 3};            // deduces vector<int>
std::pair    p{42, 3.14};           // deduces pair<int, double>
std::array   a{1, 2, 3, 4};        // deduces array<int, 4>  (C++17)
std::optional o{std::string{"hi"}}; // deduces optional<string>

// Explicit template arguments disable CTAD
std::vector<int> w{1, 2, 3};       // fine, no deduction needed

CTAD activates only when no template arguments are supplied at all. Writing std::vector<> v{1,2,3} is a syntax error, not CTAD.

Deduction guides

The compiler synthesizes implicit deduction guides from the class's constructors. These work well for simple forwarding constructors but fail for aggregates (no constructor), converting constructors that change the type, and constructors that take iterator ranges. An explicit deduction guide extends or overrides this process.

cpp
template <typename T>
struct OwningSpan {
    T*          data;
    std::size_t size;

    // Constructor taking a std::vector β€” T must be deduced from it
    explicit OwningSpan(std::vector<T>& vec)
        : data{vec.data()}, size{vec.size()} {}
};

// Explicit deduction guide: OwningSpan(vector<T>&) -> OwningSpan<T>   // C++17
template <typename T>
OwningSpan(std::vector<T>&) -> OwningSpan<T>;

std::vector<double> dv{1.0, 2.0, 3.0};
OwningSpan span{dv};   // deduces OwningSpan<double>

std::array CTAD relies on a deduction guide in the standard library that requires all element arguments to share the same type:

cpp
std::array a1{10, 20, 30};         // C++17: array<int, 3>
std::array a2{1.0f, 2.0f};         // array<float, 2>
// std::array a3{1, 2.0};          // ill-formed: conflicting deduced types

CTAD requires C++17 or later. In C++14 and earlier, the constructor arguments never trigger template argument deduction for the class template itself; you must provide the arguments explicitly or use a factory function. When writing library code that must stay compatible with C++14, continue providing make_* helpers alongside CTAD guides so both audiences are served.

Deduction guides do not participate in overload resolution the way constructors do. A guide that matches a construction expression is not guaranteed to win β€” it merely influences which template arguments are deduced before the constructor is actually called. The constructor must still exist and be callable with the deduced type.

Template Template Parameters

A template template parameter takes a template name (not an instantiation) as its argument. This is distinct from passing an already-instantiated type: std::vector vs std::vector<int>. The use case is policy-based and container-agnostic designs where the container's element type is supplied separately.

cpp
// Container is a template that takes one type argument.
// C++03 required `class`; C++17 allows `typename`.
template <template <typename> class Container, typename T>  // C++98 spelling
class Adapter {
public:
    void push(T val) { data_.push_back(std::move(val)); }
    T    pop()       { T v = data_.back(); data_.pop_back(); return v; }
    bool empty() const { return data_.empty(); }

private:
    Container<T> data_;
};

// Usage: supply the template name, then the element type
Adapter<std::vector, int>    vec_adapter;
Adapter<std::deque,  double> deq_adapter;

In C++17 the class / typename distinction in template template parameter syntax was relaxed: template <typename> typename Container is now permitted (previously only class was allowed after the inner parameter list). This is a minor syntactic cleanup; the semantics are unchanged.

When the container has more than one template parameter (e.g., std::vector<T, Allocator>), the template template parameter must match that arity:

cpp
// Matches containers with two template parameters (element + allocator)
template <template <typename, typename> class Container, typename T>
class AdapterWithAlloc {
    Container<T, std::allocator<T>> data_;
};

AdapterWithAlloc<std::vector, int>  v;
AdapterWithAlloc<std::deque,  int>  d;

C++17 variadic template template parameters (template <typename...> class C) allow matching containers of any arity without enumerating the parameter count.

Non-Type Template Parameters (NTTPs)

Non-type template parameters bind a compile-time value rather than a type. The value becomes part of the type: std::array<int, 4> and std::array<int, 8> are two distinct types even though both hold int.

Classic integer NTTPs (C++98)

The most common NTTPs are std::size_t or int constants used to size arrays or control loop unrolling:

cpp
template <typename T, std::size_t N>       // C++98
class FixedStack {
    T           data_[N];
    std::size_t top_ = 0;
public:
    void push(const T& v) { assert(top_ < N); data_[top_++] = v; }
    T    pop()            { assert(top_ > 0); return data_[--top_]; }
    bool empty() const    { return top_ == 0; }
    bool full()  const    { return top_ == N; }
};

FixedStack<int, 8>  small;   // array of 8 ints, entirely on the stack
FixedStack<int, 64> large;   // array of 64 ints
// Both are separate types β€” no implicit conversion between them.

std::array<int, N> is the standard-library incarnation of this pattern.

auto NTTPs (C++17)

C++17 added auto as an NTTP type, allowing the parameter to accept any compatible constant without specifying the type up front:

cpp
template <auto Value>                      // C++17
struct Constant {
    static constexpr auto value = Value;
    using type = decltype(Value);
};

Constant<42>     ci;   // Constant<int, 42>  β€” value is int
Constant<3.14>   cd;   // Constant<double, 3.14>
Constant<true>   cb;   // Constant<bool, true>
Constant<'Z'>    cc;   // Constant<char, 'Z'>

static_assert(Constant<42>::value == 42);

This is particularly useful for integral constant wrappers and policy tags without requiring the caller to name the type.

Pointer and reference NTTPs

Pointers and references to objects with static storage duration have always been valid NTTPs, though they see limited use:

cpp
// Classic: pointer-to-function NTTP
template <void (*Func)(int)>               // C++98
class Callback {
public:
    void invoke(int x) { Func(x); }
};

void my_handler(int n) { std::cout << n << '\n'; }
Callback<my_handler> cb;
cb.invoke(42);

Structural class-type NTTPs (C++20)

C++20 generalized NTTPs to allow literal class types that satisfy the structural requirement: all base classes and non-static data members are public, non-mutable, and themselves structural types. This enables fixed-size string literals as template arguments.

cpp
// Fixed-string wrapper satisfying structural requirements  // C++20
template <std::size_t N>
struct FixedString {
    char data[N]{};

    constexpr FixedString(const char (&str)[N]) {
        std::copy_n(str, N, data);
    }

    constexpr bool operator==(const FixedString&) const = default;
};

// Deduction guide so FixedString{"hello"} works
template <std::size_t N>
FixedString(const char (&)[N]) -> FixedString<N>;

// Class template taking a fixed-string NTTP
template <FixedString Tag>                 // C++20
class Tagged {
public:
    static constexpr std::string_view tag() { return {Tag.data}; }
};

Tagged<"player"> player_entity;
Tagged<"enemy">  enemy_entity;

static_assert(Tagged<"player">::tag() == "player");
// Tagged<"player"> and Tagged<"enemy"> are different types at compile time.

This technique is used in logging frameworks, serialization systems, and compile-time string-keyed registries where each distinct string label must produce a distinct type.

Floating-point NTTPs are also new in C++20: template <double Eps> is now valid where it was forbidden before. The structural requirement for floating-point values follows IEEE 754 bit-level equality, so two NaN values that compare equal via == may still be different template arguments.

requires Constraints on Class Templates (C++20)

Concepts and requires clauses attach constraints to class templates that the compiler checks before instantiation. Violations produce a short, targeted diagnostic instead of a cascade of substitution errors.

Constrained type parameters with requires

cpp
#include <concepts>

template <typename T>
    requires std::is_arithmetic_v<T>   // C++20 trailing requires clause
class NumericWrapper {
public:
    explicit NumericWrapper(T v) : value_{v} {}
    T get() const { return value_; }
    NumericWrapper operator+(const NumericWrapper& rhs) const {
        return NumericWrapper{value_ + rhs.value_};
    }
private:
    T value_;
};

NumericWrapper<int>    ni{42};     // OK
NumericWrapper<float>  nf{3.14f}; // OK
// NumericWrapper<std::string> ns; // error: constraint not satisfied

Abbreviated template syntax with concepts

When a concept replaces typename in the parameter list directly, no requires clause is needed:

cpp
template <std::integral T>             // C++20 β€” abbreviated syntax
class IntStack {
public:
    void push(T val) { data_.push_back(val); }
    T    top()  const { return data_.back(); }
    bool empty() const { return data_.empty(); }
private:
    std::vector<T> data_;
};

IntStack<int>      is;   // OK β€” int satisfies std::integral
IntStack<unsigned> us;   // OK
// IntStack<double> ds;  // error: double does not satisfy std::integral

std::integral, std::floating_point, std::copyable, std::equality_comparable, and std::totally_ordered are the most commonly used standard concepts for class template constraints.

Compound constraints

Multiple requirements can be composed inline. The && and || operators on concepts support subsumption, allowing the compiler to rank partial specializations and overloads by constraint strength β€” more constrained templates are preferred:

cpp
template <typename T>
concept Serializable = requires(T t, std::ostream& os) {
    { t.serialize(os) } -> std::same_as<void>;
};

template <typename T>
    requires std::copyable<T> && Serializable<T>   // C++20
class PersistentQueue {
public:
    void enqueue(T val) { data_.push_back(std::move(val)); }
    void save(std::ostream& os) const {
        for (const auto& item : data_) item.serialize(os);
    }
private:
    std::vector<T> data_;
};

The compiler checks std::copyable<T> && Serializable<T> before attempting to instantiate PersistentQueue. Failing the check at the template boundary gives a one-line message pointing directly at the unsatisfied concept, rather than an error buried deep inside save().

Member Function Templates

A member function template is a function template that is a member of a non-template class (or of a class template). The function-level type parameter is independent of any class-level parameters.

Templated member on a non-template class

cpp
class Converter {
public:
    template <typename To, typename From>
    static To convert(From val) {                  // C++98
        return static_cast<To>(val);
    }

    template <typename T>
    void print(const T& val) const {
        std::cout << val << '\n';
    }
};

Converter c;
int    i = Converter::convert<int>(3.7);   // explicit To, From deduced
double d = Converter::convert<double>(42);
c.print("hello");   // T deduced as const char*
c.print(3.14);      // T deduced as double

Templated member on a class template

When the owning class is itself a template, member function templates add a second layer of parameterization. The function template's parameter is deduced separately at each call:

cpp
template <typename T>
class Container {
    std::vector<T> data_;
public:
    void push(const T& v) { data_.push_back(v); }

    // Member template: convert to a different element type
    template <typename U>
    Container<U> as() const {                      // C++98
        Container<U> result;
        for (const auto& item : data_)
            result.push(static_cast<U>(item));
        return result;
    }
};

Container<int>    ci;
ci.push(1); ci.push(2); ci.push(3);

Container<double> cd = ci.as<double>();   // T=int, U=double
Container<float>  cf = ci.as<float>();    // T=int, U=float

Explicit specialization of a member template

A member function template can be fully specialized for a specific argument, but it cannot be partially specialized. Define the specialization outside the class body with template<>:

cpp
template <typename T>
class Formatter {
public:
    template <typename Stream>
    void write(Stream& s, const T& val) {
        s << val;
    }
};

// Full specialization of the member template for Stream = std::wostringstream
template <>
template <>
void Formatter<int>::write<std::wostringstream>(
    std::wostringstream& s, const int& val)
{
    s << L"int:" << val;
}

Member function templates cannot be virtual. The vtable is built at class definition time; the number of virtual functions must be fixed and known before any template instantiation. Attempting to mark a member function template virtual is a compile error.

Variable Templates (C++14)

A variable template declares a family of variables parameterized by type or non-type arguments. The most visible application is the _v shorthand for type-trait ::value members throughout <type_traits>.

cpp
template <typename T>
inline constexpr T pi =                            // C++14
    T(3.141592653589793238462643383279502884L);

float  area_f  = pi<float>       * r * r;
double area_d  = pi<double>      * r * r;
// Without the L suffix the source constant would be a double,
// losing precision before the conversion to long double.

Every _v helper in <type_traits> (C++17) is implemented exactly this way:

cpp
// How the standard library defines is_integral_v (representative)
template <typename T>
inline constexpr bool is_integral_v =              // C++17
    std::is_integral<T>::value;

// Usage β€” cleaner than the ::value form
static_assert(std::is_integral_v<int>);
static_assert(!std::is_integral_v<double>);

Variable templates integrate naturally with class templates β€” they can reference class template parameters and produce per-type constants without instantiating a full auxiliary class:

cpp
template <typename T>
inline constexpr std::size_t alignment_of_v =      // C++17
    alignof(T);

template <typename T, std::size_t Align>
struct AlignedBuffer {
    static_assert(Align >= alignment_of_v<T>,
                  "Align must be at least the natural alignment of T");
    alignas(Align) std::byte storage[sizeof(T)];
};

Variable templates also support partial specialization, enabling compile-time property tables:

cpp
template <typename T>
inline constexpr bool is_std_vector = false;       // C++17 β€” primary

template <typename T, typename A>
inline constexpr bool is_std_vector<std::vector<T, A>> = true;  // partial

static_assert(is_std_vector<std::vector<int>>);
static_assert(!is_std_vector<std::deque<int>>);

Always use inline constexpr in headers (C++17+). A variable template without inline in a header violates the ODR when included in multiple translation units. In C++14 codebases that cannot use inline, put variable template definitions in a single translation unit or use static constexpr local variables as a workaround.

Best Practices

Put everything in headers. Out-of-line definitions in .cpp files compile without complaint but fail to link for any translation unit that instantiates the template. The only sound alternative is explicit instantiation of every required specialization.

Prefix dependent type names with typename. Inside a template, any name that depends on a template parameter and denotes a type requires the typename keyword β€” the compiler cannot determine without it whether Container::value_type is a type or a static data member:

cpp
template <typename Container>
void drain(Container& c) {
    typename Container::value_type item;  // typename required
    while (!c.empty()) {
        item = c.front();
        c.pop();
    }
}

Use typename over class in parameter lists for clarity. Both are semantically identical in this context, but typename signals that the parameter accepts any type, including primitives β€” class implies user-defined types to many readers.

Provide deduction guides for non-trivial constructors (C++17). Implicit guides handle simple forwarding constructors well; aggregates and converting constructors often need help.

Prefer concepts to SFINAE for new C++20 code. Concepts participate in overload resolution and subsumption, produce better diagnostics, and compose without the arcane enable_if machinery.

Use inline constexpr for variable templates in headers (C++17). A variable template without inline placed in a shared header silently violates ODR. The inline qualifier allows the linker to merge identical specializations across translation units.

Common Pitfalls

Splitting declaration and definition across .h/.cpp without explicit instantiation. This is the most common beginner mistake. The linker error appears only when client code tries to use the template, far from the root cause.

Forgetting typename on dependent type names. The error message ("expected a type") can be cryptic. Any T::something that is a type needs typename T::something inside a template body.

Accidental deep instantiation chains. Recursive template metaprogramming can exhaust compiler memory and hit instantiation depth limits. Prefer std::index_sequence and fold expressions (C++17) over recursive templates:

cpp
// Prefer (C++17 fold expression)
template <typename... Ts>
void print_all(Ts&&... args) {
    ((std::cout << args << '\n'), ...);
}

Relying on implicit member instantiation to mask missing operations. Class template member functions are instantiated only when called. A type that lacks a required operation compiles fine until that member is invoked β€” errors appear late, in unexpected translation units.

Using std::enable_if in C++20 codebases. enable_if works but is strictly harder to read, write, and compose than concepts. In any project targeting C++20 or later, reach for concepts first.

Declaring virtual member function templates. The vtable requires a fixed, known set of entries at class definition time. A member function template generates an unbounded family of functions β€” the compiler cannot build the vtable. This is a hard error; use a non-template virtual interface and delegate to a templated free function instead.

Partial specialization that is not strictly more constrained. A partial specialization must be more restrictive than the primary. Writing a partial specialization whose pattern matches exactly the same set of arguments as the primary β€” or writing two partial specializations that are equally specific for some instantiation β€” produces an ambiguity error at the point of use, not at the specialization itself.

See Also

  • Template Specialization β€” full and partial specialization of class templates
  • Variable Templates β€” template<typename T> constexpr T pi and the _v pattern (C++14)
  • Concepts β€” writing and applying concept constraints (C++20)
  • SFINAE β€” substitution-failure mechanics that concepts largely supersede
  • Detection Idiom β€” probing for type members and operations at compile time
  • ADL β€” how argument-dependent lookup interacts with template friend functions