Templates and Generic Programming
Function templates, class templates, non-type template parameters, and a complete generic Stack<T> container.
Why templates
Without templates, writing a max function that works for both int and double would mean writing (and maintaining) two nearly identical functions differing only in their parameter types — or falling back to void* and casting, throwing away type safety entirely. Templates let you write one piece of code — a function or a whole class — parameterized over a type, and have the compiler generate a fully type-checked, specialized version for each concrete type it's actually used with, at compile time, with no runtime overhead at all. This is C++'s core mechanism for generic programming, and it's the foundation the entire Standard Template Library (covered on the next page) is built on.
Function templates
#include <iostream>
template <typename T>
T maxOf(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << maxOf(3, 7) << "\n"; // 7 — T deduced as int
std::cout << maxOf(3.5, 2.1) << "\n"; // 3.5 — T deduced as double
std::cout << maxOf('a', 'z') << "\n"; // z — T deduced as char
}
template <typename T> declares T as a placeholder type — typename and class are interchangeable here for this purpose (template <class T> means exactly the same thing). The compiler figures out what T actually is from the arguments at each call site (template argument deduction), so you almost never have to spell it out explicitly:
std::cout << maxOf<double>(3, 7.5) << "\n"; // 7.5 — explicit T, forcing both args to double
Critically, maxOf<int> and maxOf<double> aren't the same function reused with different types at runtime — the compiler generates two entirely separate, independently type-checked functions at compile time (a process called template instantiation), one for each type it's actually invoked with. This is exactly why templates carry no runtime cost: by the time the program runs, there's no generic dispatch happening at all, just ordinary, fully-typed function calls that happen to have been generated from a shared source.
Class templates
A class template parameterizes an entire class over one or more types, so the same class definition works for any type that supports the operations the class actually needs:
template <typename T>
class Box {
public:
explicit Box(T value) : value_(value) {}
T get() const { return value_; }
void set(T value) { value_ = value; }
private:
T value_;
};
int main() {
Box<int> intBox(42);
Box<std::string> stringBox("hello");
std::cout << intBox.get() << "\n"; // 42
std::cout << stringBox.get() << "\n"; // hello
}
Box<int> and Box<std::string> are, again, genuinely distinct compiler-generated types — intBox and stringBox don't share a runtime representation of any kind, and the compiler catches any type mismatch (calling intBox.set("oops"), for instance) as an ordinary compile error, exactly as if Box<int> had been hand-written specifically for int.
A complete example: a generic stack
Putting function templates and a class template together, here's a genuinely useful generic container — a fixed-capacity stack that works for any element type:
#include <iostream>
#include <stdexcept>
#include <vector>
template <typename T>
class Stack {
public:
void push(const T& value) {
items_.push_back(value);
}
T pop() {
if (items_.empty()) {
throw std::out_of_range("pop() called on an empty Stack");
}
T top = items_.back();
items_.pop_back();
return top;
}
bool isEmpty() const {
return items_.empty();
}
size_t size() const {
return items_.size();
}
private:
std::vector<T> items_;
};
int main() {
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
std::cout << intStack.size() << "\n"; // 3
std::cout << intStack.pop() << "\n"; // 3 — last in, first out
std::cout << intStack.pop() << "\n"; // 2
Stack<std::string> stringStack;
stringStack.push("first");
stringStack.push("second");
std::cout << stringStack.pop() << "\n"; // second
Stack<int> empty;
try {
empty.pop(); // throws — empty.isEmpty() would have avoided this
} catch (const std::out_of_range& e) {
std::cout << "Caught: " << e.what() << "\n";
}
}
This one Stack<T> definition works correctly, with full compile-time type checking, for int, std::string, or any other type — including types you write yourself — with zero code duplication and zero runtime type-checking overhead. Notice it's implemented on top of std::vector<T> internally, itself a class template — this layered reuse of generic components is the normal, idiomatic way generic C++ code is built.
Non-type template parameters
A template parameter doesn't have to be a type — it can be a compile-time constant value, most commonly used for a fixed size known up front:
template <typename T, size_t N>
class FixedArray {
public:
T& operator[](size_t index) { return data_[index]; }
size_t size() const { return N; }
private:
T data_[N];
};
int main() {
FixedArray<int, 5> arr; // N = 5 baked in at compile time, part of the TYPE itself
arr[0] = 10;
std::cout << arr.size() << "\n"; // 5
}
FixedArray<int, 5> and FixedArray<int, 10> are different types entirely, each with its size fixed and known at compile time — this is exactly how std::array<T, N> (covered on the STL page) is implemented under the hood.
Common mistakes
- Writing a template function's implementation in a
.cppfile rather than the header — templates are typically instantiated at compile time wherever they're used, so a template's full definition normally needs to be visible in every translation unit that uses it, which means it belongs in the header, not a separately-compiled source file. - Assuming a class template is one single compiled entity —
Box<int>andBox<double>are entirely separate types generated independently, each fully type-checked on its own; a bug that only shows up for one instantiation won't necessarily show up for another. - Forgetting
typename/classare interchangeable in a template parameter list, and being confused when seeing both used across different codebases for exactly the same purpose. - Reaching for a template when a simple base class and virtual functions (runtime polymorphism) would model the problem more naturally — templates buy compile-time genericity with zero runtime cost, but at the cost of every distinct instantiation being compiled separately, which can noticeably increase build times and binary size in a large project.
Interview questions
Q: What actually happens when you call maxOf<int>(3, 7) versus maxOf<double>(3.5, 2.1) — is it the same function running twice?
No — the compiler generates two entirely separate, fully type-checked functions at compile time, one instantiated for int and one for double (a process called template instantiation), each independently compiled as if it had been hand-written for that specific type. This is exactly why templates have zero runtime overhead: by the time the program actually runs, there's no generic dispatch happening at all, only ordinary calls to concrete, already-generated functions.
Q: Why does a class template's implementation usually need to live in the header file rather than a separate .cpp file?
Because the compiler needs to see a template's full definition at the point where it's instantiated for a particular type, and that instantiation can happen in any translation unit that uses the template — not just the one where it was originally written. Splitting the implementation into a separately-compiled .cpp file (the normal pattern for ordinary, non-template classes) would mean other files including only the header have no definition to instantiate from, producing a linker error.