SECTION 1
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.
1979 • C with Classes
1986 • Templates Proposed
1990 • Cfront 3.0
1998 • C++98 Standard & STL
std::vector<T> and algorithms like std::sort.2011–2020 • Modern Template Evolution
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.
SECTION 2
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.KEY TAKEAWAY
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.SECTION 3
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.
SECTION 4
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.
BASIC CLASS TEMPLATE FORM
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.
OUT-OF-CLASS MEMBER DEFINITION
The 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.
Common Pitfall
.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.SECTION 5
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"); |
Key syntax elements of class templates
SECTION 6
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>.
Implementing Stack<T>
- 1
Step 1 — Declare the Template and Data Members
We begin with the template prefix and define a privatestd::vector<T>to store the elements. Usingvectorinstead of a raw array gives us dynamic sizing automatically.template <typename T> class Stack { private: std::vector<T> elems_; };Template declared with one type parameterTand a vector-based backing store. - 2
Step 2 — Define push() and pop()
Thepushmethod takes aconst T&and delegates tovector::push_back. Thepopmethod checks for emptiness (defensive programming) before callingvector::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(); }Two mutating methods defined inline within the class body. - 3
Step 3 — Define top() and isEmpty()
Thetopmethod returns a const reference to the last element. Returning by reference avoids copying large objects. TheisEmptymethod delegates directly tovector::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(); }Non-mutating accessors:top()returnsconst T&;isEmpty()returnsbool. - 4
Step 4 — Instantiate and Use the Template
Inmain()we instantiateStack<int>andStack<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 }Output:42followed byworld. The compiler generated two independentStackclasses, one forintand one forstd::string.
SECTION 7
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 |
Comparison of generic programming approaches in C++
KEY TAKEAWAY
SECTION 8
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 |
From introductory to advanced template features
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
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?Show answer
Stack<int> and Stack<double> share no inheritance relationship and have different internal representations (the vector stores int vs. double). Practically, this means you cannot assign a Stack<int> to a Stack<double> without defining an explicit conversion. A function that accepts Stack<int> will not accept Stack<double>; you would need a function template to write type-generic functions over template instantiations.Question 2: BASIC CALCULATION
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.Show answer
template <typename T>
class Box {
T value_;
public:
Box(const T& v) : value_(v) {}
T getValue() const { return value_; }
void setValue(const T& v) { value_ = v; }
};
Instantiation: Box<int> intBox(42); creates a Box holding an integer. Box<std::string> strBox("hello"); creates a Box holding a string. The compiler generates two separate classes from this single template definition, each with fully type-checked operations.Question 3: INTERMEDIATE
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.Show answer
T must support operator< (or operator>) and be copy-constructible. This is the 'implicit interface' of templates—the compiler will only check these requirements when the template is actually instantiated with a specific type.
template <typename T>
class MinMax {
T min_, max_;
bool initialized_ = false;
public:
void insert(const T& val) {
if (!initialized_) {
min_ = max_ = val;
initialized_ = true;
} else {
if (val < min_) min_ = val;
if (val > max_) max_ = val;
}
}
T getMin() const { return min_; }
T getMax() const { return max_; }
};
If you instantiate MinMax<SomeType> where SomeType does not define operator<, the compiler will produce an error only when insert() is called.Question 4: APPLIED
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.Show answer
template <typename T>
class SensorLog {
std::vector<T> buffer_;
size_t capacity_, head_ = 0, count_ = 0;
public:
explicit SensorLog(size_t cap) : buffer_(cap), capacity_(cap) {}
void record(const T& reading) {
buffer_[head_] = reading;
head_ = (head_ + 1) % capacity_;
if (count_ < capacity_) ++count_;
}
const T& latest() const {
return buffer_[(head_ + capacity_ - 1) % capacity_];
}
};
Making N a constructor parameter (as shown) allows different buffer sizes at runtime from the same class instantiation. Making it a non-type template parameter (template <typename T, size_t N>) would fix the size at compile time, potentially enabling stack allocation and better optimization, but SensorLog<float, 10> and SensorLog<float, 20> would be different types, reducing flexibility in heterogeneous collections.Question 5: CRITICAL THINKING
Show answer
Stack<int>, Stack<double>, etc.) generates distinct machine code, which can increase binary size—a phenomenon called 'template bloat.'
This becomes a genuine concern in embedded systems with limited flash memory, or in large-scale projects with hundreds of instantiations. Mitigation strategies include: (1) factoring type-independent logic into a non-template base class (the 'thin template' idiom), (2) using explicit instantiation declarations (extern template) to control where instantiations occur, (3) designing templates to delegate to type-erased implementations where performance is not critical, and (4) relying on the linker's ability to deduplicate identical instantiations (COMDAT folding). Modern compilers and linkers handle this well in practice, and for most applications the compile-time type safety and zero-overhead abstraction far outweigh the binary size cost.SUMMARY
Lesson Summary
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.