C++ Templates From Scratch: Generic Programming Explained Simply
Introduction: What Problem Do Templates Solve?
Templates are C++‘s most powerful feature, yet they often intimidate newcomers. Here’s the core idea: templates enable you to write code once and apply it to many different types without sacrificing type safety.
Imagine you want to implement a stack. A stack of integers works the same way as a stack of strings, which works the same way as a stack of custom objects. Without templates, you’d need to copy your implementation for each type. With templates, you write once and instantiate for many types.
Templates solve the fundamental tension in programming:
- Type safety: You don’t want runtime errors from wrong types
- Code reuse: You don’t want to duplicate code for each type
Templates give you both.
In this guide, we’ll build a practical understanding of C++ templates, from basic function templates to advanced metaprogramming, with clear examples at each step.
The “Before Templates” World: Function Overloading Limitations
Before templates existed, programmers used function overloading. Let’s see why this doesn’t scale:
// Function overloading - requires separate implementation for each type
int getMax(int a, int b) {
return (a > b) ? a : b;
}
double getMax(double a, double b) {
return (a > b) ? a : b;
}
float getMax(float a, float b) {
return (a > b) ? a : b;
}
string getMax(string a, string b) {
return (a > b) ? a : b;
}
// Usage
int main() {
cout << getMax(5, 10) << "\n"; // Calls int version
cout << getMax(3.14, 2.71) << "\n"; // Calls double version
cout << getMax(string("apple"), string("banana")) << "\n"; // Calls string version
return 0;
}
Problems:
- Code duplication: The same logic repeated for each type
- Maintenance nightmare: Fix a bug? Update all versions
- Doesn’t scale: Add a new custom type? Write another overload
- No type flexibility: You must know all types at compile time
Templates elegantly solve this problem.
Function Templates: The Simplest Templates
A function template is a blueprint for functions. The compiler instantiates (creates) the actual function for each type you use it with.
// Function template - write once, use for any type!
template<typename T>
T getMax(T a, T b) {
return (a > b) ? a : b;
}
int main() {
// Compiler creates int version: int getMax<int>(int, int)
cout << getMax(5, 10) << "\n";
// Compiler creates double version: double getMax<double>(double, double)
cout << getMax(3.14, 2.71) << "\n";
// Compiler creates string version: string getMax<string>(string, string)
cout << getMax(string("apple"), string("banana")) << "\n";
// Explicit template argument (usually not needed)
cout << getMax<float>(1.5f, 2.5f) << "\n";
return 0;
}
Output:
10
3.14
banana
2.5
The beauty: one template definition, multiple type implementations. The compiler handles it automatically.
Multiple Template Parameters
Templates can have multiple type parameters:
// Two type parameters
template<typename T, typename U>
T findInVector(const vector<U>& vec, const U& target) {
for (int i = 0; i < vec.size(); i++) {
if (vec[i] == target) {
return static_cast<T>(i); // Return position
}
}
return -1; // Not found
}
int main() {
vector<string> names = {"Alice", "Bob", "Charlie"};
// Find index of "Bob" in vector of strings
int pos = findInVector<int>(names, "Bob");
cout << "Found at position: " << pos << "\n";
return 0;
}
Output:
Found at position: 1
How Template Type Deduction Works
In most cases, you don’t need to specify template arguments explicitly. The compiler deduces them from the arguments you pass:
template<typename T>
void print(T value) {
cout << "Value: " << value << "\n";
}
int main() {
print(42); // T deduced as int
print(3.14); // T deduced as double
print("hello"); // T deduced as const char*
print(string("world")); // T deduced as string
return 0;
}
Type Deduction Rules (Simplified)
- Exact match preferred: If the argument exactly matches a type, use it
- Promotion allowed: int → double, char → int
- Qualification adjustment:
Tcan deduceconst TorT&
template<typename T>
void demonstrate(T value) {
// T's type is deduced from value
}
int main() {
int x = 5;
demonstrate(x); // T = int
demonstrate(&x); // T = int*
demonstrate(string("hi")); // T = string
// const and references affect deduction
const int cx = 10;
demonstrate(cx); // T = const int (const preserved!)
return 0;
}
Class Templates: Generic Data Structures
Class templates work the same way as function templates—they’re blueprints for classes.
// Generic stack template
template<typename T>
class Stack {
private:
vector<T> elements;
public:
void push(const T& value) {
elements.push_back(value);
}
T pop() {
if (elements.empty()) {
throw runtime_error("Stack is empty");
}
T value = elements.back();
elements.pop_back();
return value;
}
bool isEmpty() const {
return elements.empty();
}
int size() const {
return elements.size();
}
};
int main() {
// Stack of integers
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
while (!intStack.isEmpty()) {
cout << intStack.pop() << " "; // Prints: 30 20 10
}
cout << "\n";
// Stack of strings
Stack<string> stringStack;
stringStack.push("one");
stringStack.push("two");
stringStack.push("three");
while (!stringStack.isEmpty()) {
cout << stringStack.pop() << " "; // Prints: three two one
}
cout << "\n";
return 0;
}
Output:
30 20 10
three two one
Template Methods in Template Classes
template<typename T>
class Container {
private:
vector<T> data;
public:
// Template method - generic within a generic class
template<typename U>
void add(const U& value) {
// Convert U to T if possible
data.push_back(static_cast<T>(value));
}
void print() const {
for (const auto& item : data) {
cout << item << " ";
}
cout << "\n";
}
};
int main() {
Container<double> nums;
nums.add(42); // int → double
nums.add(3.14f); // float → double
nums.add(2.718); // double → double
nums.print(); // 42 3.14 2.718
return 0;
}
Template Specialization: Customizing for Specific Types
Sometimes you need different behavior for specific types. That’s where template specialization comes in.
Full Specialization
// Generic template
template<typename T>
class Printer {
public:
void print(const T& value) {
cout << value << "\n";
}
};
// Full specialization for bool
template<>
class Printer<bool> {
public:
void print(const bool& value) {
cout << (value ? "true" : "false") << "\n";
}
};
// Full specialization for vector<T>
template<typename T>
class Printer<vector<T>> {
public:
void print(const vector<T>& vec) {
cout << "Vector: [";
for (size_t i = 0; i < vec.size(); i++) {
cout << vec[i];
if (i < vec.size() - 1) cout << ", ";
}
cout << "]\n";
}
};
int main() {
Printer<int> intPrinter;
intPrinter.print(42);
Printer<bool> boolPrinter;
boolPrinter.print(true);
Printer<vector<int>> vecPrinter;
vecPrinter.print({1, 2, 3, 4, 5});
return 0;
}
Output:
42
true
Vector: [1, 2, 3, 4, 5]
Partial Specialization
Partial specialization lets you specialize templates for categories of types:
// Generic template
template<typename T>
class Container {
public:
void info() {
cout << "Generic container\n";
}
};
// Partial specialization for pointers
template<typename T>
class Container<T*> {
public:
void info() {
cout << "Container of pointers\n";
}
};
// Partial specialization for vector
template<typename T>
class Container<vector<T>> {
public:
void info() {
cout << "Container of vectors\n";
}
};
int main() {
Container<int> c1;
c1.info(); // Generic container
Container<int*> c2;
c2.info(); // Container of pointers
Container<vector<int>> c3;
c3.info(); // Container of vectors
return 0;
}
Output:
Generic container
Container of pointers
Container of vectors
Default Template Parameters
Templates can have default parameters, just like functions:
template<typename T = int, typename U = double>
class Pair {
private:
T first;
U second;
public:
Pair(T f, U s) : first(f), second(s) {}
void print() const {
cout << "(" << first << ", " << second << ")\n";
}
};
int main() {
Pair<> p1(5, 3.14); // Uses defaults: int, double
p1.print();
Pair<int, int> p2(1, 2); // Override both
p2.print();
Pair<string> p3("hello", 2.71); // Override first, use default second
p3.print();
return 0;
}
Output:
(5, 3.14)
(1, 2)
(hello, 2.71)
Variadic Templates: Parameter Packs
Variadic templates (C++11+) let templates accept a variable number of arguments:
// Base case - when no arguments left
template<typename T>
T sum(T value) {
return value;
}
// Recursive case - processes first argument, delegates rest
template<typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
int main() {
cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
cout << sum(1.5, 2.5, 3.5) << "\n"; // 7.5
cout << sum(string("a"), string("b"), string("c")) << "\n"; // abc
return 0;
}
Output:
15
7.5
abc
Unpacking Parameter Packs
// Print all arguments
template<typename... Args>
void printAll(Args... args) {
// Use fold expression (C++17)
(..., (cout << args << " ")); // Left fold
cout << "\n";
}
int main() {
printAll(1, 2.5, "hello", true); // 1 2.5 hello 1
return 0;
}
Template Metaprogramming: Compile-Time Computations
Templates are Turing-complete—they can do computations at compile time. This is powerful but advanced.
Factorial at Compile Time
// Compile-time factorial using template specialization
template<int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
// Base case specialization
template<>
struct Factorial<0> {
static constexpr int value = 1;
};
int main() {
// All computed at compile time!
cout << "5! = " << Factorial<5>::value << "\n"; // Computes: 120
cout << "10! = " << Factorial<10>::value << "\n"; // Computes: 3628800
// This is type-safe and zero runtime overhead
constexpr int fiveFactorial = Factorial<5>::value;
return 0;
}
Output:
5! = 120
10! = 3628800
Type Traits: Querying Type Information
// Check if a type is integral (compiler built-in)
#include <type_traits>
template<typename T>
void process(T value) {
if constexpr (is_integral_v<T>) {
cout << "Processing integer: " << value << "\n";
} else if constexpr (is_floating_point_v<T>) {
cout << "Processing float: " << value << "\n";
} else {
cout << "Processing other type\n";
}
}
int main() {
process(42); // Processing integer: 42
process(3.14); // Processing float: 3.14
process("text"); // Processing other type
return 0;
}
Concepts (C++20): Constraining Templates Cleanly
Concepts let you specify requirements for template parameters. This makes templates more readable and errors clearer.
// Define a concept - what types must support
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> convertible_to<T>; // Must support +
};
template<typename T>
concept Printable = requires(T x) {
{ cout << x }; // Must be printable
};
// Use concepts to constrain templates
template<Addable T>
T add(T a, T b) {
return a + b;
}
template<Printable T>
void safePrint(T value) {
cout << "Value: " << value << "\n";
}
int main() {
cout << add(5, 10) << "\n"; // Works - int is Addable
cout << add(3.5, 2.5) << "\n"; // Works - double is Addable
safePrint(42); // Works - int is Printable
safePrint("hello"); // Works - const char* is Printable
// Compile error if concept not satisfied
// struct NonAddable {};
// add(NonAddable(), NonAddable()); // ERROR!
return 0;
}
Output:
15
6
Value: 42
Value: hello
Common Template Errors and How to Read Them
Template error messages are notoriously long. Here’s how to parse them:
template<typename T>
T getFirst(T arr[]) {
return arr[0];
}
int main() {
vector<int> vec = {1, 2, 3};
// ERROR: vector doesn't decay to pointer
// getFirst(vec);
// To fix: write vector-aware template
template<typename T>
T getFirstVector(const vector<T>& vec) {
return vec[0];
}
int arr[] = {1, 2, 3};
cout << getFirst(arr) << "\n"; // Works - arrays decay to pointers
return 0;
}
How to read error messages:
- Look for “template instantiation” - tells you which template was used
- Look for “no matching function” - template didn’t match your arguments
- Look at the constraints - does your type satisfy requirements?
typename vs class in Templates
Both typename and class work in template declarations:
// These are equivalent
template<typename T>
class Container1 {};
template<class T>
class Container2 {};
// Difference: typename is more explicit about intent
template<typename T> // "T is a type"
class Container3 {};
// Use typename in type-dependent contexts
template<typename T>
void print(T value) {
// When T is a class, this accesses a nested type
typename T::iterator it; // 'typename' tells compiler this is a type
}
Modern C++ prefers typename for clarity, but class is valid for backward compatibility.
Header-Only Implementation Requirement
Templates must be fully defined in headers. You cannot separate declaration and definition across files:
// stack.h
#ifndef STACK_H
#define STACK_H
#include <vector>
using namespace std;
template<typename T>
class Stack {
private:
vector<T> elements;
public:
// Definition MUST be in header
void push(const T& value) {
elements.push_back(value);
}
T pop() {
T value = elements.back();
elements.pop_back();
return value;
}
};
#endif
// This is wrong - don't do this:
// stack.cpp - DON'T PUT TEMPLATE DEFINITIONS HERE
// template<typename T>
// void Stack<T>::push(const T& value) { ... }
// Won't compile when you include stack.h in other files!
Why? When you use Stack<int>, the compiler must see the full template definition to instantiate it. If the definition is in a .cpp file, it’s not available when compiling your code.
Practical Example: Implementing a Generic Stack
Let’s build a complete, production-ready template:
#include <iostream>
#include <vector>
#include <stdexcept>
#include <type_traits>
using namespace std;
template<typename T>
class Stack {
private:
vector<T> elements;
public:
// Push element onto stack
void push(const T& value) {
elements.push_back(value);
}
// Pop and return top element
T pop() {
if (isEmpty()) {
throw runtime_error("Stack underflow");
}
T value = elements.back();
elements.pop_back();
return value;
}
// Peek at top element without removing
const T& peek() const {
if (isEmpty()) {
throw runtime_error("Stack is empty");
}
return elements.back();
}
// Check if stack is empty
bool isEmpty() const {
return elements.empty();
}
// Get number of elements
size_t size() const {
return elements.size();
}
// Clear stack
void clear() {
elements.clear();
}
// Template method for custom operations
template<typename Func>
void forEach(Func operation) const {
for (const auto& element : elements) {
operation(element);
}
}
};
int main() {
// Integer stack
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
cout << "Integer stack (LIFO order):\n";
while (!intStack.isEmpty()) {
cout << intStack.pop() << " ";
}
cout << "\n\n";
// String stack
Stack<string> stringStack;
stringStack.push("world");
stringStack.push("hello");
cout << "String stack:\n";
cout << stringStack.pop() << " " << stringStack.pop() << "\n\n";
// Using forEach with lambda
Stack<int> nums;
nums.push(1);
nums.push(2);
nums.push(3);
cout << "Elements using forEach:\n";
nums.forEach([](int x) { cout << x << " "; });
cout << "\n\n";
// Error handling
try {
Stack<int> emptyStack;
emptyStack.pop(); // Throws
} catch (const runtime_error& e) {
cout << "Caught error: " << e.what() << "\n";
}
return 0;
}
Output:
Integer stack (LIFO order):
30 20 10
String stack:
hello world
Elements using forEach:
3 2 1
Caught error: Stack underflow
Advanced: Stack Specialization for Booleans
// Specialization for vector<bool> (which is optimized)
template<>
class Stack<bool> {
private:
vector<bool> bits;
public:
void push(bool value) {
bits.push_back(value);
}
bool pop() {
if (isEmpty()) throw runtime_error("Stack underflow");
bool value = bits.back();
bits.pop_back();
return value;
}
bool isEmpty() const { return bits.empty(); }
size_t size() const { return bits.size(); }
void printBinary() const {
for (bool bit : bits) {
cout << (bit ? '1' : '0');
}
cout << "\n";
}
};
int main() {
Stack<bool> bitStack;
bitStack.push(true);
bitStack.push(false);
bitStack.push(true);
bitStack.push(true);
cout << "Bits (bottom to top): ";
bitStack.printBinary(); // 1011
return 0;
}
Conclusion: Templates Are Powerful Tools
Templates enable you to write generic, type-safe code that the compiler specializes for your needs. Key takeaways:
- Function templates let you write functions once for many types
- Class templates create generic data structures
- Type deduction usually eliminates the need to specify template arguments
- Specialization lets you customize templates for specific types
- Header-only implementation is required—templates must be fully defined in headers
- Variadic templates handle variable numbers of arguments
- Concepts (C++20) make template requirements explicit
- Template metaprogramming enables compile-time computation
The learning curve is steep, but templates are worth mastering. They’re fundamental to modern C++ design and unlock capabilities impossible in other languages.
Next steps: Explore the Standard Template Library (STL). It’s built entirely on templates and demonstrates best practices. Try implementing other data structures like Queue, LinkedList, and HashMap as templates.
What template challenge are you facing? Templates take practice, but each problem you solve builds intuition.
Take Your C++ Further
If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.
👉 Get the C++ Better Explained Ebook — $19