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.

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.

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.

See Also

  • Template Specialization β€” full and partial specialization of class templates
  • 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