C++ Class Templates: Out-of-Line Members, Partial Specialization, Alias Templates and CTAD

Introduction: Int stack, double stack—copy/paste forever?

Stop duplicating Stack for every type

You wrote IntStack, DoubleStack, same logic—only T differs. Class templates are the cookie cutter: one pattern, many baked shapes. You write Stack, Stack, Stackstd::string once; the compiler instantiates a concrete class at each use.

// g++ -std=c++17 -o stack_tpl stack_tpl.cpp && ./stack_tpl
#include <vector>
#include <iostream>
#include <string>
#include <stdexcept>
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(const T& value) { data.push_back(value); }
    T pop() {
        if (empty()) throw std::logic_error("Stack is empty");
        T value = data.back();
        data.pop_back();
        return value;
    }
    bool empty() const { return data.empty(); }
    size_t size() const { return data.size(); }
};
int main() {
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    std::cout << intStack.pop() << "\n";
    Stack<std::string> strStack;
    strStack.push("hello");
    strStack.push("world");
    std::cout << strStack.pop() << "\n";
    return 0;
}

Unlike function templates, you usually name the template arguments explicitly: Stack. C++17 CTAD can deduce in some cases from constructors.

It helps to be precise about what a class template is: it is not a class. Stack on its own has no size, no member functions you can call, and no code in the binary. Only Stack<int> is a class, created the first time the compiler needs it. Each instantiation is a completely separate type, so Stack<int> and Stack<long> share nothing at run time and cannot be assigned to each other, and a std::vector<Stack<int>> cannot hold a Stack<double>. The compiler also instantiates member functions lazily: pop() is only compiled for Stack<T> if some code calls it. That is why a class template can contain a member that would not compile for a particular T (say, a print() that uses operator<<) and still be used with that T, as long as nobody calls the offending member.

Two details of the example are deliberate. pop() returns the element by value, which copies (or moves) it before the vector shrinks. std::stack instead splits this into top() and pop() returning void, because a pop() that returns by value cannot be exception-safe for types whose copy constructor can throw: if copying the return value throws after the element was removed, the element is lost. For a teaching example returning the value is more convenient; for a general-purpose container, the standard library’s split is the safer design. And push(const T&) always copies; adding an overload push(T&&) or a variadic emplace(Args&&...) avoids a copy for temporaries and move-only types like std::unique_ptr.

flowchart TB
  subgraph template[Template definition]
    T["template <typename T>\nclass Stack"]
  end
  subgraph instances[Instantiated types]
    I1[Stack<int>]
    I2[Stack<double>]
    I3[Stack<std::string>]
  end
  T -->|T=int| I1
  T -->|T=double| I2
  T -->|T=std::string| I3

Basic syntax

template <typename T>
class Box {
    T value;
public:
    Box(const T& v) : value(v) {}
    T get() const { return value; }
    void set(const T& v) { value = v; }
};

Multiple parameters:

template <typename K, typename V>
class KeyValue { /* ... */ };

Default template arguments:

template <typename T, typename Container = std::vector<T>>
class Stack { /* ... */ };

Inside the class body, the name Box without arguments refers to the current instantiation (the “injected class name”), so a copy constructor can be written Box(const Box& other) instead of Box(const Box<T>& other). Default template arguments work like default function arguments: they must come after the parameters without defaults, and a later default can refer to an earlier parameter, as Container = std::vector<T> does. The default is what lets users write Stack<int> while still allowing Stack<int, std::deque<int>> when they need a different backing store. Template parameters do not have to be types; size_t N in the ring buffer below is a non-type template parameter, and C++20 extends those to floating-point values and simple class types.


Out-of-line member definitions

template <typename T>
void Container<T>::set(const T& v) {
    value = v;
}

Templates: definitions typically stay in headers (same TU) to avoid link errors. Static data members:

template <typename T>
int Counter<T>::count = 0;

Defining a member outside the class needs two things: the template <typename T> header again, and the qualified name Container<T>:: with the arguments spelled out, because outside the class body the compiler does not know which instantiation you mean. Return types that are nested types of the template need typename as well, for example template <typename T> typename List<T>::iterator List<T>::begin(), or more readably with a trailing return type, auto List<T>::begin() -> iterator, where iterator is looked up in the class scope.

The header rule is where most beginners first get stuck. If you put the class template in stack.h and its member definitions in stack.cpp like a normal class, everything compiles, and then the link step fails with undefined reference to 'Stack<int>::push(int const&)'. The reason is the lazy instantiation described above: when stack.cpp is compiled, nobody asks for Stack<int>, so no code is generated, and when main.cpp asks for it, the definitions are not visible. Keeping definitions in the header (inside or below the class) fixes it. The alternative, explicit instantiation, is covered in the production section.

Each distinct T gets its own copy of a static data member: Counter<int>::count and Counter<double>::count are separate variables. That is often exactly what you want (a per-type instance counter), but it surprises people who expected one counter for all instantiations; if that is the goal, move the static into a non-template base class. Since C++17 you can also write inline static int count = 0; inside the class and drop the out-of-line definition entirely.


Partial specialization

Specialize for patterns (e.g. T*, T[], Pair<T,T>)—not in the primary template. Note: A common pattern uses SmartPtr<T[]>-style partial specialization for array delete semantics—match your design to delete vs delete[].

template <typename T>
struct Describe { static const char* name() { return "value"; } };

template <typename T>                       // partial: any pointer type
struct Describe<T*> { static const char* name() { return "pointer"; } };

template <typename T>                       // partial: any std::vector
struct Describe<std::vector<T>> { static const char* name() { return "vector"; } };

template <>                                 // full: exactly bool
struct Describe<bool> { static const char* name() { return "bool"; } };

// Describe<int>::name()               -> "value"
// Describe<int*>::name()              -> "pointer"
// Describe<std::vector<double>>::name() -> "vector"
// Describe<bool>::name()              -> "bool"

A full specialization (template <>) replaces the template for one exact set of arguments; a partial specialization still has template parameters and matches a whole family of types. When several specializations match, the compiler picks the most specialized one, and if two are equally specialized the use is ambiguous and fails to compile. A specialization is a completely independent class: it does not inherit any members from the primary template, so every member it needs has to be written again. That is why large specializations are often implemented by deriving from a common base, or by specializing only a small traits class and keeping one main implementation.

Two restrictions catch people. Function templates cannot be partially specialized at all, only overloaded (a common workaround is to delegate to a static member of a class template, which can be). And specializations must be declared before the first use that would instantiate the primary template with those arguments; otherwise the program is ill-formed and, worse, different translation units may silently use different versions. In the standard library, std::unique_ptr<T[]> is the textbook partial specialization: it replaces operator* and operator-> with operator[] and calls delete[] instead of delete, exactly the “array delete semantics” mentioned above.


Template aliases

template <typename T>
using Vec = std::vector<T>;
template <typename K, typename V>
using Map = std::unordered_map<K, V>;
template <typename T>
using StringMap = std::unordered_map<std::string, T>;

An alias template gives a short name to a family of types, and it is a true alias: Vec<int> is std::vector<int>, not a new type, so the two are interchangeable everywhere. Its main advantage over the C++03 workaround (template <typename T> struct Vec { typedef std::vector<T> type; };, used as typename Vec<T>::type) is that it removes both the ::type suffix and the typename keyword at every use. The standard library’s _t helpers such as std::remove_reference_t<T> are alias templates built exactly this way.

StringMap shows the other common use: fixing some parameters of a template and leaving others open, which is a kind of partial application. What an alias cannot do is be specialized; if you need “a different type for some T”, specialize a helper struct and alias its member instead. Also note that CTAD does not work through alias templates before C++20, so Vec v{1, 2, 3}; needs C++20, while std::vector v{1, 2, 3}; works from C++17.

Class template argument deduction (CTAD)

Since C++17, the compiler can deduce class template arguments from constructor arguments, the way it always could for function templates:

std::pair p{1, 2.5};              // std::pair<int, double>
std::vector v{1, 2, 3};           // std::vector<int>
Box b{std::string("hi")};         // Box<std::string>
// Box empty;                     // error: no argument to deduce T from

Deduction is driven by the constructors (implicitly generated “deduction guides”), and you can add explicit guides when the constructor signature does not reveal the type directly, for example template <typename It> Stack(It, It) -> Stack<typename std::iterator_traits<It>::value_type>; for an iterator-pair constructor. The traps are the same as with brace initialization: std::vector v{5} deduces std::vector<int> with one element, and with the Box(const T&) constructor above, Box b{"hi"} deduces T as char[3], because an array does not decay when it binds to a reference, and Box<char[3]> then fails to compile. The standard types avoid this with deduction guides that take arguments by value, which is why std::pair p{"hi", 1} gives std::pair<const char*, int>; neither is std::string, which has to be requested explicitly. When the deduced type is not obvious at a glance, spelling out the arguments is still clearer.


Practical generic containers

A fixed-capacity ring buffer is a good example of a class template that earns its genericity — the same logic works for int, std::string, or any movable type, and the capacity itself can be a non-type template parameter.

template <typename T, size_t Capacity>
class RingBuffer {
    std::array<T, Capacity> data{};
    size_t head_ = 0, count_ = 0;
public:
    void push(const T& value) {
        data[(head_ + count_) % Capacity] = value;
        if (count_ < Capacity) ++count_;
        else head_ = (head_ + 1) % Capacity;  // overwrite oldest
    }
    T& front() { return data[head_]; }
    size_t size() const { return count_; }
    bool full() const { return count_ == Capacity; }
};
// RingBuffer<int, 4> for a fixed-size event log
// RingBuffer<std::string, 16> for a recent-commands history

Why a class template here: a function template can’t hold state between calls, and a non-template class would force one copy-pasted RingBuffer per (T, Capacity) pair.

Making the capacity a template parameter means the storage lives inside the object (std::array), with no heap allocation, and the capacity is a compile-time constant the optimizer can use; if Capacity is a power of two, the % Capacity becomes a cheap bit mask. The trade-off is that RingBuffer<int, 4> and RingBuffer<int, 8> are different types, so a function that should accept “any ring buffer of ints” has to be a template itself, and choosing the capacity at run time is impossible.

The phrase “any movable type” needs a qualifier. std::array<T, Capacity> data{} value-initializes every slot up front, so T must be default-constructible, and push uses copy assignment, so T must be copy-assignable. A type without a default constructor, or a move-only type like std::unique_ptr, does not compile with this version. Supporting those requires raw storage (alignas(T) std::byte buf[sizeof(T) * Capacity]) with placement new and manual destruction, which is how production ring buffers are written. Also, front() on an empty buffer returns a default-constructed element rather than signaling an error, so callers must check size() first.


Complete Stack/Array/traits example

Putting partial specialization, template aliases, and traits together in one self-contained example:

#include <vector>
#include <array>
#include <type_traits>
// Primary template: growable stack backed by std::vector
template <typename T, typename Container = std::vector<T>>
class Stack {
    Container data;
public:
    void push(const T& v) { data.push_back(v); }
    void pop() { data.pop_back(); }
    T& top() { return data.back(); }
    bool empty() const { return data.empty(); }
};
// Partial specialization: fixed-size stack for a raw array backing store
template <typename T, size_t N>
class Stack<T, std::array<T, N>> {
    std::array<T, N> data{};
    size_t count_ = 0;
public:
    void push(const T& v) { if (count_ < N) data[count_++] = v; }
    void pop() { if (count_ > 0) --count_; }
    T& top() { return data[count_ - 1]; }
    bool empty() const { return count_ == 0; }
};
// Trait: detect whether a type is stack-like (has push/pop/top)
template <typename, typename = void>
struct is_stack_like : std::false_type {};
template <typename T>
struct is_stack_like<T, std::void_t<decltype(std::declval<T>().push(std::declval<typename T::value_type>()))>>
    : std::true_type {};
int main() {
    Stack<int> heapStack;            // uses std::vector<int> backing
    heapStack.push(1);
    heapStack.push(2);
    Stack<int, std::array<int, 8>> fixedStack;  // uses partial specialization
    fixedStack.push(1);
    fixedStack.push(2);
    return 0;
}

This mirrors how the standard library itself composes: std::stack is a container adapter over a configurable backing container, exactly like the Stack<T, Container> shown here.

The partial specialization Stack<T, std::array<T, N>> is selected whenever the second argument is an std::array of the same T. The compiler deduces T and N from the pattern, so Stack<int, std::array<int, 8>> picks the fixed-size version without any extra syntax. This is the kind of situation partial specialization exists for: std::array has no push_back or pop_back, so the primary template would not compile with it, and the specialization supplies a different implementation behind the same interface. The fixed version makes its own choices that a caller should know about: push on a full stack silently drops the value, and top() on an empty stack reads data[-1] through an unsigned wrap, which is undefined behavior. Real code would assert, throw, or return a status.

The is_stack_like trait uses the classic void_t detection idiom: the partial specialization is only viable if the expression inside decltype is valid for T, otherwise substitution fails and the primary template (false_type) is used. As written, though, it checks T::value_type, and neither Stack above declares a value_type, so is_stack_like<Stack<int>>::value is false, which is the opposite of what the name promises. Adding using value_type = T; to both versions fixes it, and it is good practice anyway, since generic code, including the standard library, expects containers to expose value_type. In C++20, the same check reads much more clearly as a concept: template <typename S> concept StackLike = requires(S s, typename S::value_type v) { s.push(v); s.pop(); s.top(); };.


Common errors

  • Undefined reference to template member defined only in .cpp → put definition in header or explicit instantiation
  • Dependent names → typename / template keyword
  • >> in nested templates → fine in C++11+ (was > > in C++03)
  • CTAD fails for default-constructed Box b when T can’t be deduced → write Box b

The dependent-name rule is the one that produces the most confusing messages. Inside a template, a name that depends on T, such as T::value_type or Container::iterator, could in principle be a type, a value, or a template, and the compiler has to decide how to parse it before T is known. The rule is that it is assumed to be a value unless you say otherwise, so T::value_type x; fails with messages like need 'typename' before 'T::value_type' because 'T' is a dependent scope. Writing typename T::value_type x; fixes it. The same applies to member templates of a dependent type: calling obj.template get<0>() needs the template keyword when obj’s type depends on T, or the < is parsed as a less-than operator. C++20 relaxed the typename requirement in contexts where only a type can appear, such as alias declarations and return types, but it is still required in many places.

Error messages from templates are long because the compiler reports the entire instantiation chain, from the line in your code down to the line inside the template where something failed. The practical way to read them is to find the first line that mentions your own file, then the line that says “required from here”. Constraining template parameters with static_assert or C++20 concepts moves the error to the point of use and makes it one line instead of fifty.


Best practices

  • Prefer typename for type parameters
  • static_assert constraints
  • using for value_type, iterators inside containers
  • if constexpr (C++17) for type-specific branches

typename and class are completely interchangeable in a template parameter list; typename is simply clearer, because the argument does not have to be a class (Stack<int> works). A static_assert(std::is_copy_constructible_v<T>, "Stack<T> requires a copyable T"); at the top of the class turns a wall of instantiation errors into one readable message, and in C++20 a concept in the parameter list (template <std::copyable T>) does the same while also taking part in overload resolution. if constexpr is often a lighter alternative to a partial specialization when only one member function needs to behave differently for some types: the discarded branch is not instantiated, so it may contain code that would not compile for the current T. Reach for a specialization when the data layout differs, as in the std::array stack, and for if constexpr when only behavior differs.


Production patterns

  • CRTP for static polymorphism
  • Policy-based design (container type as template parameter)
  • Explicit instantiation in .cpp for selected types to reduce compile time:
template class Stack<int>;
template class Stack<std::string>;

Explicit instantiation turns the header-only rule around. You keep only declarations in the header, put the member definitions in stack.cpp, and add these lines at the bottom of that file; the compiler then generates all members of Stack<int> and Stack<std::string> in that one translation unit, and other files link against them. It shortens builds when a heavy template is used with a small, known set of types, and it keeps the implementation out of the header. The cost is that only the listed types work: Stack<double> in another file compiles and then fails to link. The complementary declaration extern template class Stack<int>; in the header tells every other file not to instantiate Stack<int> itself, which is how you avoid the same instantiation being compiled in hundreds of files while still keeping the definitions visible.

CRTP (the curiously recurring template pattern) uses a class template that takes the derived class as its parameter, class Widget : public Printable<Widget>, so the base can call derived-class methods through static_cast<Derived&>(*this) without virtual functions. It is how many libraries add shared behavior (comparison operators, counters, interface checks) with zero run-time cost; C++23’s deducing this covers some of the same ground more simply.


Class template syntax at a glance

TopicDetail
Syntaxtemplate <typename T> class C { };
UseC<int> obj; (CTAD exceptions in C++17)
MembersOut-of-line definitions need template<typename T> and C<T>::
Partial specPattern-based specializations
Aliasesusing Alias = Template<T>;

One class template replaces many copy-pasted classes; specialize patterns; keep definitions visible to the compiler.

FAQ

When is this useful?

A. Building reusable containers, wrappers, and type-safe APIs—core of STL-style design. A good signal is when you notice two classes that differ only in the type of a member or parameter. If they also differ in behavior for some types, add a specialization or if constexpr rather than a second class.

Read first?

A. Template basics covers function templates and argument deduction, which this article builds on.

A. The member function definitions are probably in a .cpp file that never instantiates the types you use. Move the definitions into the header, or add explicit instantiations (template class Stack<int>;) for every type you need in that .cpp file.