Historical Context & Motivation
Before templates existed in C++, writing a generic container—say, a stack that could hold integers, doubles, or strings—required either duplicating the entire class definition for each type or resorting to void* pointers that sacrificed type safety. Both approaches were error-prone, tedious to maintain, and fundamentally at odds with C++'s philosophy of zero-cost abstractions. The desire for type-safe generic programming without runtime overhead drove the development of templates as a core language feature.
std::vector<T> and algorithms like std::sort.The central question that class templates answer is deceptively simple: how can we write a single class definition that the compiler specializes for each concrete type at compile time, preserving full type safety and incurring no runtime overhead? Understanding class templates is essential because they form the backbone of the STL and virtually every modern C++ library.
Core Principles & Definitions
A class template is a blueprint for generating classes. Rather than defining a concrete class, you define a pattern parameterized by one or more template parameters—most commonly type parameters introduced with typename or class. When you write MyClass<int>, the compiler performs template instantiation: it substitutes int for every occurrence of the type parameter and generates a fully concrete class. This process happens entirely at compile time, which means template-generated code runs at the same speed as hand-written, type-specific code.
Template Declaration
template <typename T> prefix introduces a type parameter T that can be used throughout the class body as a placeholder for any type.Implicit Instantiation
Stack<double>, the compiler automatically generates a concrete class by substituting double for T. Only member functions that are actually called get instantiated.Type Safety
void* approaches, the compiler enforces that a Stack<int> only accepts int values. Misuse is caught at compile time, not at runtime.Code Reuse Without Inheritance
Header-Only Convention
.h or .hpp), not split into .cpp files.Stack<int> cookie and a Stack<string> cookie have the same structural shape but hold entirely different data, and both are produced from the same single cutter definition.Visual Explanation — From Template to Concrete Classes
T, producing three entirely separate classes—Stack<int>, Stack<double>, and Stack<string>—each with its own member variable types.The diagram above illustrates the fundamental mechanism of template instantiation. Notice that every occurrence of T in the template body is replaced with the concrete type argument. The resulting classes—Stack<int> and Stack<double>—are entirely distinct types in the C++ type system; you cannot assign one to the other. This is a critical distinction from runtime polymorphism via inheritance, where a base pointer can refer to any derived class. With templates, the compiler resolves everything statically, and there is no shared base class unless you explicitly design one.
How Class Templates Work — Syntax & Instantiation
Template Declaration Syntax
Every class template begins with a template parameter list enclosed in angle brackets, preceded by the keyword template. Within this list, each type parameter is introduced with either typename or class—the two keywords are interchangeable in this context. The parameter name (conventionally T, U, etc.) then serves as a placeholder throughout the class body.
T — a type parameter that will be replaced by the actual type at instantiation. ClassName — the name of the template, which is not itself a type until instantiated.Defining Member Functions Outside the Class
When you define a member function outside the template class body, you must repeat the template parameter list and qualify the class name with ClassName<T>. This tells the compiler that the function belongs to the template, not to some ordinary class. Forgetting this qualification is one of the most common errors encountered by students new to templates.
template <typename T> prefix must precede every out-of-class member definition. The scope resolution uses ClassName<T>:: rather than just ClassName::.Instantiation and the One-Definition Rule
When the compiler encounters ClassName<int> obj;, it triggers implicit instantiation. The compiler substitutes int for T and generates the full class. Crucially, only those member functions that are actually called are instantiated—unused member functions remain uncompiled. This means a template can contain member functions that would fail to compile for certain types, as long as those functions are never invoked with those types. The One-Definition Rule (ODR) still applies, but the standard permits identical template instantiations in multiple translation units; the linker deduplicates them.
.h and .cpp file the way you would with an ordinary class will result in linker errors ("undefined reference"). Because the compiler needs the full definition to instantiate the template, the entire template—including member function bodies—must be visible in every translation unit that uses it. The standard practice is to define everything in the header file.Anatomy of a Complete Class Template
To solidify the syntax, let us examine a complete class template for a simple Pair class that stores two values of potentially different types. This demonstrates multiple template parameters, constructor definitions, and accessor methods—all the essential pieces you will encounter in real-world template code.
Pair<T1, T2> class template with two type parameters. The annotations on the right highlight five key structural elements: the template parameter list, data members whose types depend on the parameters, a constructor taking const references, accessor functions whose return types match the parameters, and the instantiation syntax at the bottom.Several details in this example merit closer attention. First, the constructor takes its parameters by const reference (const T1&), which avoids unnecessary copies when T1 or T2 is a large or expensive-to-copy type such as std::string. Second, the accessor methods are marked const because they do not modify the object's state. Third, the template uses two independent type parameters, meaning Pair<int, double> and Pair<double, int> are distinct types—the order of the type arguments matters.
| Syntax Element | Meaning | Example |
|---|---|---|
template <typename T> | Declares a template with one type parameter | template <typename T> class Box { T val; }; |
ClassName<int> | Instantiates the template with a concrete type | Box<int> b; |
ClassName<T>::func() | Scope-qualified out-of-class member definition | template<typename T> T Box<T>::get() { ... } |
template <typename T1, typename T2> | Multiple type parameters | Pair<int, string> p(1, "a"); |
Worked Example — Building a Generic Stack
Let us build a Stack<T> class template from scratch, exercising all the patterns discussed so far. The stack will support push, pop, top, and isEmpty operations, backed by a std::vector<T>.
std::vector<T> to store the elements. Using vector instead of a raw array gives us dynamic sizing automatically.
template <typename T>
class Stack {
private:
std::vector<T> elems_;
};T and a vector-based backing store.push method takes a const T& and delegates to vector::push_back. The pop method checks for emptiness (defensive programming) before calling vector::pop_back.
void push(const T& elem) {
elems_.push_back(elem);
}
void pop() {
if (elems_.empty())
throw std::out_of_range("Stack::pop(): empty stack");
elems_.pop_back();
}top method returns a const reference to the last element. Returning by reference avoids copying large objects. The isEmpty method delegates directly to vector::empty.
const T& top() const {
if (elems_.empty())
throw std::out_of_range("Stack::top(): empty stack");
return elems_.back();
}
bool isEmpty() const {
return elems_.empty();
}top() returns const T&; isEmpty() returns bool.main() we instantiate Stack<int> and Stack<std::string> to verify that the same template works with fundamentally different types.
int main() {
Stack<int> intStack;
intStack.push(7);
intStack.push(42);
std::cout << intStack.top() << '\n'; // 42
Stack<std::string> strStack;
strStack.push("hello");
strStack.push("world");
std::cout << strStack.top() << '\n'; // world
}42 followed by world. The compiler generated two independent Stack classes, one for int and one for std::string.Class Templates vs. Alternatives
Class templates are not the only mechanism for writing type-generic code in C++. Before templates, programmers relied on preprocessor macros, void* pointers, or inheritance hierarchies with virtual functions. Each approach trades off differently along the axes of type safety, runtime performance, and code maintainability. The table below provides a systematic comparison.
| Criterion | Class Templates | void* Approach | Inheritance + Virtual |
|---|---|---|---|
| Type Safety | Full compile-time type checking; errors caught before execution | No type safety; incorrect casts cause undefined behavior at runtime | Type-safe through base pointer, but downcasting (dynamic_cast) needed for specific types |
| Runtime Overhead | Zero — all resolved at compile time | Minimal pointer indirection, but manual casts add fragility | Virtual dispatch via vtable; pointer indirection on every call |
| Code Bloat Risk | Each instantiation generates new code; potential binary size increase | Single implementation shared across all types | Single implementation; vtable adds small per-class overhead |
| Error Messages | Historically verbose; C++20 concepts greatly improve diagnostics | Errors surface at runtime, not compile time | Clear compile-time errors through interface contracts |
| Flexibility | Works with any type that satisfies the implicit interface (duck typing at compile time) | Works with any type, but no constraints | Requires explicit inheritance from a common base class |
Connection to Advanced Template Features
The basic class template introduced in this lesson is the foundation upon which a rich ecosystem of advanced C++ techniques is built. Understanding where these introductory concepts lead will help you appreciate why mastering the fundamentals is so important. The table below maps each introductory concept to its advanced counterpart.
| Introductory Concept | Advanced Extension | What It Enables |
|---|---|---|
Single type parameter <typename T> | Non-type template parameters | Parameterize on values (e.g., Array<int, 10>) for compile-time-sized containers |
| Implicit interface (duck typing) | C++20 Concepts | Explicit constraints on template parameters with clear error messages when requirements are not met |
| One definition for all types | Template Specialization | Override the template for specific types (e.g., optimize Stack<bool> to use bitwise packing) |
| Fixed number of type parameters | Variadic Templates | Accept an arbitrary number of type arguments (e.g., std::tuple<int, double, string>) |
| Compile-time type substitution | Template Metaprogramming | Use templates as a Turing-complete compile-time computation system for code generation and optimization |
As you progress through your C++ coursework, you will encounter these advanced features naturally. The key insight is that every advanced template technique is an extension of the basic mechanism covered in this lesson: define a parameterized pattern, let the compiler instantiate it for specific arguments. Whether you are constraining parameters with concepts, specializing for edge cases, or computing values at compile time with constexpr and template recursion, you are always building on the same foundational model of compile-time code generation from parameterized blueprints.
Practice Problems
Stack<int> and Stack<double> are considered entirely different types by the C++ compiler, even though they originate from the same class template. What are the practical implications of this for assignment and function argument passing?Box<T> that stores a single value of type T with a constructor, a getValue() accessor, and a setValue() mutator. Show how to instantiate it for both int and std::string.MinMax<T> that tracks the minimum and maximum of values inserted via an insert(const T&) method, and provides getMin() and getMax() accessors. What implicit requirements does this design place on the type T? Write the full implementation.float for temperature, int for packet counts, std::string for status messages). Design a class template SensorLog<T> that stores up to N recent readings in a circular buffer (use a std::vector<T> with modular indexing). Provide record(const T&) and latest() methods. Discuss the design decision of making N a constructor parameter versus a template parameter.Lesson Summary
A class template is a parameterized blueprint that the compiler uses to generate type-specific classes through template instantiation. You declare a template with the template <typename T> prefix, use T as a placeholder type throughout the class body, and instantiate it by specifying a concrete type in angle brackets (e.g., MyClass<int>). Each instantiation produces a distinct, independent type with full compile-time type safety and zero runtime overhead.
Key practical details include: defining template member functions with the ClassName<T>:: scope qualifier, placing the complete template definition in header files so the compiler has access to the full definition at every point of use, and understanding that only called member functions are instantiated. Class templates form the foundation of the Standard Template Library (STL) and are the gateway to advanced techniques including template specialization, variadic templates, and C++20 concepts.