π Lesson 15: Templates
Learn how to write generic C++ code that works with any type β from simple function templates to variadic templates and compile-time metaprogramming.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Write function templates that work with any type through automatic type deduction.
- Build class templates for generic containers and data structures.
- Apply full and partial template specialization to customize behavior for specific types.
- Use non-type template parameters to configure templates at compile time.
- Write variadic templates that accept any number of arguments.
- Understand the fundamentals of template metaprogramming and SFINAE.
Estimated Time: 90β120 minutes
Project: Build a generic Stack<T> class with dynamic sizing, iterator support, and a specialization for bool.
In This Lesson
What Are Templates?
Imagine a cookie cutter - you can use the same shape to make cookies from different doughs: chocolate, vanilla, or gingerbread. Templates are like cookie cutters for code - one pattern that works with many types!
Function Templates: Generic Functions
Function templates let you write one function that works with any type - like a universal adapter!
Function Template Examples
// Simple function template
// (named myMax so it doesn't clash with std::max when using namespace std;)
template<typename T>
T myMax(T a, T b) {
return (a > b) ? a : b;
}
// Multiple type parameters
// The trailing return type (-> decltype(a + b)) is a C++11 idiom.
// Since C++14 you can drop it entirely and let the compiler deduce
// the return type: template<typename T, typename U> auto add(T a, U b) { return a + b; }
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// Template with arrays
template<typename T, int SIZE>
T sum(T (&arr)[SIZE]) {
T total = 0;
for (int i = 0; i < SIZE; i++) {
total += arr[i];
}
return total;
}
// Using the templates
int main() {
// Type deduction
cout << myMax(10, 20) << endl; // T = int
cout << myMax(3.14, 2.71) << endl; // T = double
cout << myMax('a', 'z') << endl; // T = char
// Explicit type specification
cout << myMax<double>(10, 3.14) << endl; // Forces double
// Mixed types
cout << add(5, 3.14) << endl; // int + double
// Array template
int numbers[] = {1, 2, 3, 4, 5};
cout << "Sum: " << sum(numbers) << endl; // SIZE deduced as 5
return 0;
}
Template Specialization
Sometimes you need a special recipe for a specific ingredient. Template specialization lets you provide custom implementations for specific types!
Template Specialization Example
// Generic template
template<typename T>
class Storage {
private:
T value;
public:
Storage(T val) : value(val) {}
void print() {
cout << "Generic storage: " << value << endl;
}
T getValue() { return value; }
};
// Full specialization for bool
template<>
class Storage<bool> {
private:
bool value;
public:
Storage(bool val) : value(val) {}
void print() {
cout << "Bool storage: " << (value ? "TRUE" : "FALSE") << endl;
}
bool getValue() { return value; }
};
// Partial specialization for pointers
template<typename T>
class Storage<T*> {
private:
T* ptr;
public:
Storage(T* p) : ptr(p) {}
void print() {
if (ptr) {
cout << "Pointer storage: *ptr = " << *ptr << endl;
} else {
cout << "Pointer storage: NULL" << endl;
}
}
T* getValue() { return ptr; }
~Storage() {
// Note: Doesn't delete - just stores pointer
}
};
Class Templates: Generic Classes
Class templates are like blueprints for blueprints - they let you create classes that work with any type!
Class Template Implementation
template<typename T>
class DynamicArray {
private:
T* data;
int size;
int capacity;
void resize() {
capacity *= 2;
T* newData = new T[capacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
delete[] data;
data = newData;
}
public:
// Constructor
DynamicArray(int initialCapacity = 10)
: size(0), capacity(initialCapacity) {
data = new T[capacity];
}
// Destructor
~DynamicArray() {
delete[] data;
}
// Copy constructor
DynamicArray(const DynamicArray& other)
: size(other.size), capacity(other.capacity) {
data = new T[capacity];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}
// Add element
void push_back(const T& value) {
if (size == capacity) {
resize();
}
data[size++] = value;
}
// Access elements
T& operator[](int index) {
if (index < 0 || index >= size) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
// Get size
int getSize() const { return size; }
// Iterator support
T* begin() { return data; }
T* end() { return data + size; }
};
// Usage
int main() {
// Array of integers
DynamicArray<int> numbers;
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
// Array of strings
DynamicArray<string> words;
words.push_back("Hello");
words.push_back("Template");
words.push_back("World");
// Using range-based for loop
for (int num : numbers) {
cout << num << " ";
}
return 0;
}
Template Parameters: Beyond Types
Templates can take more than just types - they can take values too! It's like a recipe that needs both ingredients AND quantities.
Non-Type Template Parameters
// Fixed-size array template
template<typename T, int SIZE>
class FixedArray {
private:
T data[SIZE]; // Stack-allocated array
public:
FixedArray() {
// Initialize all elements
for (int i = 0; i < SIZE; i++) {
data[i] = T();
}
}
T& operator[](int index) {
if (index < 0 || index >= SIZE) {
throw out_of_range("Index out of bounds");
}
return data[index];
}
constexpr int size() const { return SIZE; }
void fill(const T& value) {
for (int i = 0; i < SIZE; i++) {
data[i] = value;
}
}
};
// Matrix template with dimensions
template<typename T, int ROWS, int COLS>
class Matrix {
private:
T data[ROWS][COLS];
public:
Matrix() {
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
data[i][j] = T();
}
}
}
T& at(int row, int col) {
return data[row][col];
}
Matrix<T, COLS, ROWS> transpose() {
Matrix<T, COLS, ROWS> result;
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
result.at(j, i) = data[i][j];
}
}
return result;
}
};
// Usage
FixedArray<int, 10> smallArray;
FixedArray<double, 1000> largeArray;
Matrix<float, 3, 3> rotation;
Matrix<int, 2, 3> data;
Variadic Templates: Variable Arguments
Variadic templates are like a recipe that can handle any number of ingredients - from a simple sandwich to a full feast!
Variadic Template Examples
// Print function for any number of arguments
template<typename T>
void print(T&& t) {
cout << t << endl;
}
template<typename T, typename... Args>
void print(T&& t, Args&&... args) {
cout << t << " ";
print(args...); // Recursive call with remaining args
}
// Type-safe printf
template<typename... Args>
void safePrintf(const string& format, Args... args) {
printf(format.c_str(), args...);
}
// Sum any number of values
template<typename T>
T sum(T t) {
return t;
}
template<typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
// Create tuple-like structure
template<typename... Types>
class Tuple {
// Implementation details...
};
// Factory function β this is essentially how the standard library's
// std::make_unique (available since C++14) is implemented. We name ours
// make_unique_ex here so it doesn't clash with std::make_unique; in real
// code you would just call std::make_unique directly.
template<typename T, typename... Args>
unique_ptr<T> make_unique_ex(Args&&... args) {
return unique_ptr<T>(new T(forward<Args>(args)...));
}
// Usage
print(1, 2.5, "Hello", 'A'); // Works with any types
cout << sum(1, 2, 3, 4, 5) << endl; // 15
auto ptr = make_unique_ex<string>("Hello, World!");
π‘ C++17 shortcut: The recursive sum above (with its single-argument base case) can collapse into one fold expression β return (args + ...); β with no base case at all. Recursion is still worth understanding, but reach for a fold when it fits.
Template Metaprogramming: Compile-Time Magic
Template metaprogramming is like having a chef (compiler) prepare ingredients (compute values) before the restaurant (program) even opens!
Template Metaprogramming Examples
// Compile-time factorial
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;
};
// Compile-time Fibonacci
template<int N>
struct Fibonacci {
static constexpr int value =
Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
template<>
struct Fibonacci<0> {
static constexpr int value = 0;
};
template<>
struct Fibonacci<1> {
static constexpr int value = 1;
};
// Type traits
// (named IsPointer/Conditional so they don't clash with the real
// std::is_pointer / std::conditional when using namespace std;)
template<typename T>
struct IsPointer {
static constexpr bool value = false;
};
template<typename T>
struct IsPointer<T*> {
static constexpr bool value = true;
};
// Conditional type selection
template<bool Condition, typename TrueType, typename FalseType>
struct Conditional {
using type = TrueType;
};
template<typename TrueType, typename FalseType>
struct Conditional<false, TrueType, FalseType> {
using type = FalseType;
};
// Usage
constexpr int fact5 = Factorial<5>::value; // 120 at compile time
constexpr int fib10 = Fibonacci<10>::value; // 55 at compile time
cout << "5! = " << fact5 << endl;
cout << "Is int* a pointer? " << IsPointer<int*>::value << endl;
// Choose type based on condition
using MyType = Conditional<sizeof(int) == 4, int, long>::type;
β¨ Modern note: reach for constexpr first
Recursive struct templates like Factorial<N> are the classic way to compute values at compile time, and they're worth understanding because you'll meet them in real code. But since C++11 (and much more capable since C++14) a plain constexpr function does the same job with ordinary, readable syntax:
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr int fact5 = factorial(5); // still computed at compile time -> 120
The standard type traits (std::is_pointer, std::conditional, and friends in <type_traits>) already ship the trait templates shown above, so you rarely need to hand-roll them β the versions here are just to reveal how they work.
SFINAE: Substitution Failure Is Not An Error
SFINAE is like a restaurant menu - if they don't have what you ordered, they don't shut down, they just say "we don't serve that"!
SFINAE with enable_if (the classic technique)
Before C++20, enable_if was the standard tool for switching a template on or off depending on the type. Each overload only becomes viable when its condition holds:
// Only viable for integral types (int, long, char, ...)
template<typename T>
typename enable_if<is_integral_v<T>, T>::type
half(T value) {
return value / 2; // integer division
}
// Only viable for floating-point types (float, double, ...)
template<typename T>
typename enable_if<is_floating_point_v<T>, T>::type
half(T value) {
return value / 2.0; // real division
}
β¨ Modern C++20: prefer concepts over SFINAE
enable_if works, but it clutters signatures and produces famously cryptic error messages. Since C++20, concepts express the same "only for these types" intent far more readably β and the compiler diagnostics point straight at the unmet requirement:
// Same behavior as the enable_if version, now self-documenting
template<integral T> // std::integral from <concepts>
T half(T value) { return value / 2; }
template<floating_point T> // std::floating_point
T half(T value) { return value / 2.0; }
// A requires clause handles custom or compound constraints
template<typename T>
requires is_integral_v<T> && (sizeof(T) >= 4)
T half(T value) { return value / 2; }
When you reach for enable_if in new code, ask first whether a concept β std::integral, std::floating_point, or one you define yourself β would say it more clearly. It almost always will.
Practice Exercise: Generic Container
ποΈ Build a Generic Stack
Create a template-based stack with the following features:
- Works with any type
- Dynamic sizing
- Exception safety
- Iterator support
- Specialization for bool to save space
template<typename T>
class Stack {
private:
// TODO: Add member variables
public:
Stack();
~Stack();
void push(const T& value);
void pop();
T& top();
const T& top() const;
bool empty() const;
size_t size() const;
// TODO: Add iterator support
class iterator {
// Iterator implementation
};
iterator begin();
iterator end();
};
// TODO: Specialize for bool
template<>
class Stack<bool> {
// Bit-packed implementation
};
π‘ Hint
- Use dynamic array internally
- Implement rule of three/five
- For bool specialization, pack 8 bools per byte
- Consider using placement new for exception safety
Modern C++ Template Features
Modern Template Features
// C++11: Auto return type
template<typename T, typename U>
auto multiply(T t, U u) -> decltype(t * u) {
return t * u;
}
// C++14: Generic lambdas
auto genericLambda = [](auto x, auto y) {
return x + y;
};
// C++17: Class template argument deduction
pair p(1, 2.5); // Deduces pair<int, double>
vector v{1, 2, 3, 4}; // Deduces vector<int>
// C++17: Fold expressions
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // Fold expression
}
// C++20: Concepts
template<typename T>
concept Numeric = requires(T a, T b) {
{ a + b } -> convertible_to<T>;
{ a - b } -> convertible_to<T>;
{ a * b } -> convertible_to<T>;
{ a / b } -> convertible_to<T>;
};
template<Numeric T>
T calculate(T a, T b) {
return (a + b) * (a - b);
}
// C++20: Abbreviated function templates β each `auto` parameter is
// an implicit template type parameter, so no template<...> header needed.
auto multiplyAny(auto a, auto b) {
return a * b;
}
// Constrain an abbreviated template by placing a concept before auto
auto halfOf(integral auto value) { // only accepts integral types
return value / 2;
}
// Constexpr if (C++17)
template<typename T>
string toString(T value) {
if constexpr (is_same_v<T, string>) {
return value;
} else if constexpr (is_arithmetic_v<T>) {
return to_string(value);
} else {
return "Unknown type";
}
}
Template Best Practices
Challenge Exercise: Expression Templates
ποΈ Advanced Template Challenge
Build a simple expression template system for vector operations:
- Lazy evaluation of expressions
- Avoid temporary objects
- Support +, -, * operations
- Efficient computation
β Solution
template<typename E>
class VecExpression {
public:
double operator[](size_t i) const {
return static_cast<const E&>(*this)[i];
}
size_t size() const {
return static_cast<const E&>(*this).size();
}
};
class Vec : public VecExpression<Vec> {
// Vector implementation
};
template<typename E1, typename E2>
class VecSum : public VecExpression<VecSum<E1, E2>> {
// Sum expression
};
π― Quick Quiz
Question 1: Which declaration correctly introduces a function template with a single type parameter T?
Question 2: In template<typename T, int SIZE> class FixedArray { T data[SIZE]; ... };, what is SIZE?
Question 3: Given the lesson's Storage<T> example, what does writing template<> class Storage<bool> { ... }; define?
Summary
π Key Takeaways
- Templates enable generic programming - write once, use with many types
- Function templates create generic functions with type deduction
- Class templates create generic classes and data structures
- Specialization allows custom implementations for specific types
- Non-type parameters enable compile-time configuration
- Variadic templates handle variable numbers of arguments type-safely
- Template metaprogramming performs computation at compile time
- SFINAE enables conditional template instantiation
- Modern features like concepts make templates easier and safer
π Additional Resources
- cppreference.com
- cppreference β Templates
- cppreference β Parameter packs (variadic templates)
- isocpp.org β Templates FAQ
π What's Next?
You now know how to write code once and have it work with any type. Next, in Lesson 16: Standard Template Library, you'll see templates at work in the wild β the STL's vector, map, set, and algorithms like sort are all built from exactly the template techniques you just learned, ready for you to use directly.
π You're a Template Master!
Generic programming is one of the hardest ideas in C++ to click β and it just clicked for you. Every reusable, type-safe library you write from here builds on what you learned in this lesson.