The moment class templates clicked for me was when I stopped thinking of std::vector<int> and std::vector<std::string> as two flavors of the same thing, and started seeing them as two entirely different, compiler-generated classes that just happen to share a single source blueprint. That mental shift is really the whole story of class templates: you write the structure and behavior of a class exactly once, using a placeholder for the type, and the compiler stamps out a fully concrete, independent class for every type you actually use.
In this guide I’ll build a class template step by step — starting from a minimal generic Box<T>, through a proper generic Stack<T> with dynamic memory management, and into multi-parameter and specialized templates. I’ll explain what the compiler is doing at each stage, cover the rule of five for template classes managing memory, and go through the debugging pitfalls that show up most often when people move from function templates to class templates.
What Is a Class Template?
A class template is a pattern for generating classes, parameterized on one or more types (or values). Instead of hand-writing a Box class for every type you need to store, you write it once:
#include <iostream>
template<typename T>
class Box {
private:
T value;
public:
Box(const T& val) : value(val) {}
T getValue() const {
return value;
}
void setValue(const T& val) {
value = val;
}
};
int main() {
Box<int> intBox(42);
Box<std::string> stringBox("Hello, Templates!");
std::cout << intBox.getValue() << "\n";
std::cout << stringBox.getValue() << "\n";
intBox.setValue(100);
std::cout << "Updated: " << intBox.getValue() << "\n";
return 0;
}
Output:
42
Hello, Templates!
Updated: 100
Box<int> and Box<std::string> are, to the compiler, two completely separate and unrelated classes. They don’t share a vtable, don’t share static members, and one can’t be implicitly converted to the other. They simply both originate from the same template definition.
Anatomy of a Class Template Declaration
template<typename T> // template parameter list
class ClassName { // the templated class body
T member; // T can be used anywhere a type is expected
public:
ClassName(T val); // constructors, methods, etc. can all use T
};
You can use class instead of typename in the parameter list (template<class T>) — they are functionally identical in this position; typename is generally preferred by convention for clarity, though both appear constantly in real codebases and the standard library itself uses both historically.
Defining Member Functions Outside the Class
When member functions are defined outside the class body, you must repeat the template parameter list and qualify the class name with <T>:
#include <iostream>
template<typename T>
class Box {
private:
T value;
public:
Box(const T& val);
T getValue() const;
};
// Out-of-class definitions
template<typename T>
Box<T>::Box(const T& val) : value(val) {}
template<typename T>
T Box<T>::getValue() const {
return value;
}
int main() {
Box<double> b(3.14159);
std::cout << b.getValue() << "\n";
return 0;
}
Output:
3.14159
This is important to internalize early: Box<T>::Box and Box<T>::getValue are not standalone functions — they’re still part of the template and get instantiated together with the rest of the class whenever a particular T is used.
Building a Complete Generic Class: A Dynamic Stack
Let’s build something with real state and memory management, since that’s where class templates actually earn their keep. Here’s a generic Stack<T> backed by a dynamically-resized array.
#include <iostream>
#include <stdexcept>
template<typename T>
class Stack {
private:
T* data;
size_t capacity;
size_t count;
void resize(size_t newCapacity) {
T* newData = new T[newCapacity];
for (size_t i = 0; i < count; ++i) {
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity = newCapacity;
}
public:
explicit Stack(size_t initialCapacity = 4)
: data(new T[initialCapacity]), capacity(initialCapacity), count(0) {}
// Rule of five: destructor
~Stack() {
delete[] data;
}
// Copy constructor
Stack(const Stack& other)
: data(new T[other.capacity]), capacity(other.capacity), count(other.count) {
for (size_t i = 0; i < count; ++i) {
data[i] = other.data[i];
}
}
// Copy assignment
Stack& operator=(const Stack& other) {
if (this == &other) return *this;
T* newData = new T[other.capacity];
for (size_t i = 0; i < other.count; ++i) {
newData[i] = other.data[i];
}
delete[] data;
data = newData;
capacity = other.capacity;
count = other.count;
return *this;
}
// Move constructor
Stack(Stack&& other) noexcept
: data(other.data), capacity(other.capacity), count(other.count) {
other.data = nullptr;
other.capacity = 0;
other.count = 0;
}
// Move assignment
Stack& operator=(Stack&& other) noexcept {
if (this == &other) return *this;
delete[] data;
data = other.data;
capacity = other.capacity;
count = other.count;
other.data = nullptr;
other.capacity = 0;
other.count = 0;
return *this;
}
void push(const T& value) {
if (count == capacity) {
resize(capacity * 2);
}
data[count++] = value;
}
void pop() {
if (count == 0) {
throw std::out_of_range("Stack::pop(): stack is empty");
}
--count;
}
T& top() {
if (count == 0) {
throw std::out_of_range("Stack::top(): stack is empty");
}
return data[count - 1];
}
bool isEmpty() const { return count == 0; }
size_t size() const { return count; }
};
int main() {
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
std::cout << "Top: " << intStack.top() << "\n";
std::cout << "Size: " << intStack.size() << "\n";
intStack.pop();
std::cout << "After pop, top: " << intStack.top() << "\n";
Stack<std::string> stringStack;
stringStack.push("first");
stringStack.push("second");
std::cout << "String stack top: " << stringStack.top() << "\n";
// Test copy
Stack<int> copyStack = intStack;
copyStack.push(99);
std::cout << "Original top: " << intStack.top() << ", Copy top: " << copyStack.top() << "\n";
return 0;
}
Output:
Top: 30
Size: 3
After pop, top: 20
String stack top: second
Original top: 20, Copy top: 99
This example demonstrates why the rule of five (destructor, copy constructor, copy assignment, move constructor, move assignment) matters especially for class templates that manage a resource like a raw dynamic array — without it, copying a Stack<T> would shallow-copy the pointer and cause a double-free when both stacks were destroyed.
Multiple Template Parameters
Class templates aren’t limited to one type parameter. A generic key-value pair, for instance:
#include <iostream>
#include <string>
template<typename K, typename V>
class KeyValuePair {
private:
K key;
V value;
public:
KeyValuePair(const K& k, const V& v) : key(k), value(v) {}
K getKey() const { return key; }
V getValue() const { return value; }
void display() const {
std::cout << key << " => " << value << "\n";
}
};
int main() {
KeyValuePair<std::string, int> p1("age", 30);
KeyValuePair<int, std::string> p2(101, "Engineering");
p1.display();
p2.display();
return 0;
}
Output:
age => 30
101 => Engineering
Default Template Arguments
Just like function default parameters, class templates can have default type arguments:
#include <iostream>
template<typename T = int, int Size = 10>
class FixedArray {
private:
T elements[Size];
public:
void set(int index, const T& val) {
if (index >= 0 && index < Size) elements[index] = val;
}
T get(int index) const { return elements[index]; }
constexpr int getSize() const { return Size; }
};
int main() {
FixedArray<> defaultArray; // T=int, Size=10
FixedArray<double, 5> customArray; // T=double, Size=5
defaultArray.set(0, 42);
customArray.set(0, 3.14);
std::cout << "Default array size: " << defaultArray.getSize() << ", val[0]: " << defaultArray.get(0) << "\n";
std::cout << "Custom array size: " << customArray.getSize() << ", val[0]: " << customArray.get(0) << "\n";
return 0;
}
Output:
Default array size: 10, val[0]: 42
Custom array size: 5, val[0]: 3.14
Note the second parameter, int Size, is a non-type template parameter — a compile-time constant value rather than a type. This is exactly the mechanism std::array<T, N> is built on.
Template Specialization
Sometimes the generic implementation is wrong (or inefficient) for a particular type. Full specialization lets you override the entire class body for one specific type:
#include <iostream>
template<typename T>
class Printer {
public:
void print(const T& value) {
std::cout << "Value: " << value << "\n";
}
};
// Full specialization for bool
template<>
class Printer<bool> {
public:
void print(const bool& value) {
std::cout << "Boolean: " << (value ? "true" : "false") << "\n";
}
};
int main() {
Printer<int> intPrinter;
Printer<bool> boolPrinter;
intPrinter.print(42);
boolPrinter.print(true);
return 0;
}
Output:
Value: 42
Boolean: true
Partial specialization lets you specialize for a category of types, such as all pointer types, while still being generic in another respect:
#include <iostream>
template<typename T>
class TypeDescriber {
public:
static void describe() {
std::cout << "A regular type\n";
}
};
// Partial specialization for pointer types
template<typename T>
class TypeDescriber<T*> {
public:
static void describe() {
std::cout << "A pointer type\n";
}
};
int main() {
TypeDescriber<int>::describe();
TypeDescriber<int*>::describe();
return 0;
}
Output:
A regular type
A pointer type
Note: partial specialization is only available for class templates, not function templates — this is a common point of confusion. Function templates can only be overloaded, not partially specialized.
Class Template Argument Deduction (CTAD, C++17)
Before C++17, you always had to specify template arguments explicitly when constructing an object: Box<int> b(5);. C++17 introduced CTAD, which lets the compiler deduce the type parameters from constructor arguments, much like it already did for function templates:
#include <iostream>
template<typename T>
class Box {
T value;
public:
Box(T val) : value(val) {}
T get() const { return value; }
};
int main() {
Box b1(42); // CTAD deduces Box<int>
Box b2(3.14); // CTAD deduces Box<double>
Box b3("hello"); // CTAD deduces Box<const char*>
std::cout << b1.get() << "\n";
std::cout << b2.get() << "\n";
std::cout << b3.get() << "\n";
return 0;
}
Output:
42
3.14
hello
If the deduced type isn’t what you want (as with Box<const char*> above, when you probably wanted Box<std::string>), you can write a deduction guide to steer CTAD:
template<typename T>
class Box {
T value;
public:
Box(T val) : value(val) {}
};
Box(const char*) -> Box<std::string>; // deduction guide (needs a matching constructor or conversion in practice)
Internal Working and Compiler Process
- Instantiation on use. The compiler doesn’t generate any code for
Box<T>in the abstract — it waits until you actually writeBox<int>orBox<std::string>somewhere, and only then generates the concrete class, including only the member functions you actually call (member functions of class templates are themselves lazily instantiated, one at a time, unlike the class’s data members which are all instantiated together). - Two-phase name lookup. Similar to function templates, the compiler checks non-dependent code at template definition time, and dependent code (anything relying on
T) at instantiation time. This is why a class template can be defined and compiled into an object file’s template metadata without ever having itsT-dependent logic type-checked against a concrete type until instantiation. - Independent static members per instantiation. Every distinct instantiation of a class template gets its own, entirely separate set of
staticmembers:
#include <iostream>
template<typename T>
class Counter {
public:
static int count;
Counter() { ++count; }
};
template<typename T>
int Counter<T>::count = 0;
int main() {
Counter<int> a, b;
Counter<double> c;
std::cout << "Counter<int>::count = " << Counter<int>::count << "\n";
std::cout << "Counter<double>::count = " << Counter<double>::count << "\n";
return 0;
}
Output:
Counter<int>::count = 2
Counter<double>::count = 1
This confirms Counter<int> and Counter<double> really are unrelated classes as far as static state is concerned — a frequent source of surprise for people expecting templates to behave like generics in languages with type erasure.
- Memory layout is fixed and concrete per instantiation.
sizeof(Box<int>)andsizeof(Box<double>)will differ, exactly as if you’d hand-written two separate classes — there’s no hidden indirection or boxing.
Best Practices
- Keep the public interface type-agnostic and minimal. A generic class should expose behavior that makes sense across the range of types it’s meant to support, not leak implementation details specific to one type.
- Always implement the rule of five (or rule of zero) for class templates that manage a resource. If your template allocates memory, opens a file, or holds any owned resource, either write all five special member functions or design it so it owns nothing directly (e.g., using
std::vectororstd::unique_ptrinternally, letting the “rule of zero” apply). - Use
expliciton single-argument constructors to avoid unintended implicit conversions, exactly as you would in a non-template class. - Prefer composition over inheritance for template classes unless you have a specific design reason — deep template class hierarchies get complicated fast, especially when combined with virtual functions (which templates can have, but which come with their own trade-offs).
- Document template parameter requirements clearly (e.g., “T must support
operator<“) — or better, constrain them explicitly with concepts in C++20. - Avoid over-parameterizing. Adding template parameters “just in case” increases compile-time complexity and usage friction without guaranteed future benefit.
Constraining Class Templates with Concepts (C++20)
#include <iostream>
#include <concepts>
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
template<Comparable T>
class SortedPair {
T first, second;
public:
SortedPair(T a, T b) {
if (a < b) { first = a; second = b; }
else { first = b; second = a; }
}
void display() const {
std::cout << first << ", " << second << "\n";
}
};
int main() {
SortedPair<int> p(5, 2);
p.display();
return 0;
}
Output:
2, 5
If you tried to instantiate SortedPair with a type lacking operator<, the compiler produces a clear “constraint not satisfied” error instead of a deep, confusing failure buried inside the class body — a significant readability win over pre-C++20 SFINAE-based constraints.
Performance Optimization
Class templates carry no inherent runtime overhead versus hand-written classes — each instantiation compiles to concrete, non-generic machine code. The performance considerations that matter in practice:
- Compile-time cost scales with the number of distinct instantiations. A class template instantiated with fifty different types produces fifty independent sets of compiled member functions.
- Header-only nature increases rebuild sensitivity. Because class templates are almost always defined entirely in headers, changing the template implementation forces recompilation of every translation unit that instantiates it.
- Explicit instantiation can reduce redundant compilation across multiple translation units for known, fixed sets of types:
// In a .cpp file:
template class Stack<int>;
template class Stack<double>;
This tells the compiler to generate Stack<int> and Stack<double> exactly once in this translation unit, which other translation units can then link against instead of each instantiating their own copies (useful in large projects to reduce build time and binary size).
Debugging Common Mistakes
Mistake 1: Forgetting to repeat the template parameter list on out-of-class definitions
template<typename T>
class Box { T value; public: T get() const; };
// WRONG:
// T Box::get() const { return value; }
// RIGHT:
template<typename T>
T Box<T>::get() const { return value; }
Mistake 2: Shallow copy bugs from missing the rule of five If a class template owns a raw pointer and you don’t define a copy constructor/assignment, the compiler-generated defaults will shallow-copy the pointer, leading to double-free or use-after-free bugs the moment two instances are destroyed.
Mistake 3: Assuming a template class instantiation is generated even if unused
template<typename T>
class Thing {
public:
void onlyValidForNumbers() { T x = 5; /* ... */ }
};
Thing<std::string> t; // fine, as long as onlyValidForNumbers() is never called
// t.onlyValidForNumbers(); // would fail to compile: std::string x = 5; is invalid
Member functions of class templates are instantiated lazily, individually, only when called — this can hide bugs until a specific method is actually invoked with a specific type.
Mistake 4: Confusing class template parameter keyword with actual class inheritance. template<class T> does not mean T must be a class type — it can be any type, including built-ins like int. class and typename are interchangeable here.
Real-World Applications
- Every STL container (
std::vector,std::map,std::list,std::unordered_map,std::array) is a class template, generally following exactly the patterns shown above (constructors, rule of five, non-type parameters forstd::array). - Smart pointers (
std::unique_ptr<T>,std::shared_ptr<T>) are class templates that add resource-management semantics on top of a generic payload type. - Custom data structures in performance-critical code (game engines, embedded systems, trading systems) are frequently written as class templates to get compile-time specialization without sacrificing code reuse.
- Policy-based design (a technique popularized by Andrei Alexandrescu) uses class templates with multiple type parameters representing swappable “policies” (e.g., a locking policy, a storage policy) to build highly configurable, zero-overhead components.
- Wrapper and adapter classes, such as thread-safe queues or bounded buffers, are commonly implemented as class templates so the same synchronization logic applies to any payload type.
Interview Questions
- What is the difference between a class template and a template instantiation?
- Why must you repeat the template parameter list when defining member functions outside the class body?
- Are static members shared across different instantiations of the same class template? Explain why or why not.
- What is the difference between full and partial specialization of a class template?
- Why is the rule of five especially important for class templates managing raw resources?
- What is CTAD, and how do deduction guides influence it?
- Can class templates have non-type template parameters? Give an example.
- Why can function templates be overloaded but not partially specialized, while class templates can be partially specialized?
FAQs
Q: Can a class template inherit from a non-template class, or vice versa? Yes to both — templates can freely mix with ordinary inheritance, as long as you’re careful about accessing dependent names (you may need this-> or typename Base<T>::member in some inheritance scenarios involving templated base classes).
Q: Do class templates support virtual functions? Yes. A class template can declare virtual member functions normally; each instantiation gets its own independent vtable.
Q: Is there a difference between template<typename T> and template<class T> for class templates? No functional difference in this position — both declare a type template parameter. typename is often preferred by convention for readability, though you’ll see both extensively in real code and in the standard library’s own historical usage.
Q: How do I limit which types a class template accepts? Use static_assert inside the class body for a basic compile-time check, std::enable_if with SFINAE pre-C++20, or concepts in C++20 for the clearest, most maintainable constraint syntax.
Q: Can I explicitly instantiate a class template to speed up builds? Yes — template class ClassName<Type>; in a single translation unit forces instantiation there, avoiding redundant instantiation across every other file that uses the same type.
Troubleshooting Tips
- If member function definitions outside the class fail to compile, double check you’ve repeated
template<typename T>immediately above and writtenClassName<T>::rather than justClassName::. - If you get a double-free or crash when copying objects of your template class, check whether you’ve implemented the rule of five, especially if the class owns a raw pointer or handle.
- If a specific method call fails to compile only for one particular type, remember that class template member functions are instantiated lazily per-call — the failure is isolated to whichever method actually got used with that type.
- If two seemingly identical uses of the same instantiation behave unexpectedly differently, check whether
staticmembers are involved — remember each distinctTgets a completely separate set of static state.
Summary and Key Takeaways
Class templates let you define a class exactly once and have the compiler generate independent, fully concrete versions for every type you actually use — with the same performance characteristics as if you’d hand-written each version separately. The concepts that matter most in practice are: instantiation happens on demand and per-member-function; every distinct type parameter produces an unrelated class with its own static state and memory layout; and any class template that manages a resource directly needs the rule of five to avoid shallow-copy bugs. Combined with non-type parameters, specialization, CTAD, and — since C++20 — concepts for clean constraints, class templates are the mechanism behind essentially every container and smart pointer in the C++ Standard Library, and they’re a foundational skill for writing reusable, type-safe C++ code.
References
- ISO/IEC 14882 (C++ Standard), Class Templates — https://www.iso.org/standard/
- cppreference: Class template — https://en.cppreference.com/w/cpp/language/class_template
- cppreference: Partial template specialization — https://en.cppreference.com/w/cpp/language/partial_specialization
- cppreference: Class template argument deduction (CTAD) — https://en.cppreference.com/w/cpp/language/class_template_argument_deduction
- GCC documentation on template instantiation — https://gcc.gnu.org/onlinedocs/gcc/Template-Instantiation.html