Templates
Templates are C++'s mechanism for generic programming — writing code once that works for any type. The STL is built entirely on templates: std::vector<T>, std::sort, std::unique_ptr<T> all work because of templates. The compiler generates type-specific code from your template at compile time — zero runtime overhead.
Function Templates
#include <iostream>
using namespace std;
// Template — T is a type parameter deduced from arguments
template <typename T>
T maxOf(T a, T b) {
return (a > b) ? a : b;
}
// Multiple type parameters
template <typename T, typename U>
auto add(T a, U b) {
return a + b; // Return type deduced (C++14)
}
// Non-type template parameter
template <int N>
void printN(const string& s) {
for (int i = 0; i < N; i++) cout << s << " ";
cout << "\n";
}
int main() {
cout << maxOf(10, 20) << "\n"; // 20 (int)
cout << maxOf(3.14, 2.71) << "\n"; // 3.14 (double)
cout << maxOf('a', 'z') << "\n"; // z (char)
cout << maxOf(string("apple"), string("banana")) << "\n"; // banana
cout << add(1, 2.5) << "\n"; // 3.5 (int + double)
cout << add(1, 2) << "\n"; // 3 (int + int)
printN<3>("Hello"); // Hello Hello Hello
printN<5>("*"); // * * * * *
return 0;
}Class Templates
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T>
class Stack {
vector<T> data;
size_t maxSize;
public:
Stack(size_t maxSize = 100) : maxSize(maxSize) {}
void push(const T& val) {
if (data.size() >= maxSize) throw overflow_error("Stack full");
data.push_back(val);
}
void push(T&& val) { // Move overload
if (data.size() >= maxSize) throw overflow_error("Stack full");
data.push_back(move(val));
}
T pop() {
if (data.empty()) throw underflow_error("Stack empty");
T val = move(data.back());
data.pop_back();
return val;
}
const T& top() const {
if (data.empty()) throw underflow_error("Stack empty");
return data.back();
}
bool empty() const { return data.empty(); }
size_t size() const { return data.size(); }
};
int main() {
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
cout << intStack.pop() << "\n"; // 30
Stack<string> strStack;
strStack.push("hello");
strStack.push("world");
cout << strStack.top() << "\n"; // world
// Works for any type
Stack<pair<int,string>> pairStack;
pairStack.push({1, "one"});
auto [n, s] = pairStack.pop();
cout << n << " " << s << "\n"; // 1 one
return 0;
}Template Specialisation
You can provide a custom implementation for specific types:
#include <iostream>
#include <string>
using namespace std;
// Primary template
template <typename T>
class Formatter {
public:
string format(const T& val) {
return to_string(val);
}
};
// Full specialisation for bool
template <>
class Formatter<bool> {
public:
string format(const bool& val) {
return val ? "true" : "false";
}
};
// Full specialisation for string (just quote it)
template <>
class Formatter<string> {
public:
string format(const string& val) {
return "\"" + val + "\"";
}
};
// Partial specialisation — for any pointer type
template <typename T>
class Formatter<T*> {
public:
string format(T* ptr) {
if (!ptr) return "null";
return "&(" + to_string(*ptr) + ")";
}
};
int main() {
Formatter<int> fi;
Formatter<bool> fb;
Formatter<string> fs;
Formatter<int*> fp;
cout << fi.format(42) << "\n"; // 42
cout << fb.format(true) << "\n"; // true
cout << fs.format("hi") << "\n"; // "hi"
int x = 100;
cout << fp.format(&x) << "\n"; // &(100)
cout << fp.format<int>(nullptr) << "\n"; // null
return 0;
}Variadic Templates
Templates that accept any number of type arguments:
#include <iostream>
#include <string>
using namespace std;
// Base case — no more arguments
void print() { cout << "\n"; }
// Variadic template — recurse on the rest
template <typename First, typename... Rest>
void print(First first, Rest... rest) {
cout << first << " ";
print(rest...); // Recurse with one fewer argument
}
// Fold expression (C++17) — much simpler
template <typename... Args>
auto sum(Args... args) {
return (... + args); // Left fold: ((a + b) + c)
}
template <typename... Args>
void printAll(Args&&... args) {
((cout << forward<Args>(args) << " "), ...); // Comma fold
cout << "\n";
}
int main() {
print(1, 2.5, "hello", true); // 1 2.5 hello 1
cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
cout << sum(1.0, 2.5, 3.14) << "\n"; // 6.64
printAll(42, "world", 3.14, true); // 42 world 3.14 1
return 0;
}Template Type Traits (C++11)
<type_traits> provides compile-time type introspection:
#include <iostream>
#include <type_traits>
using namespace std;
template <typename T>
void typeInfo() {
cout << " is_integral: " << is_integral_v<T> << "\n";
cout << " is_floating_point: " << is_floating_point_v<T> << "\n";
cout << " is_pointer: " << is_pointer_v<T> << "\n";
cout << " is_class: " << is_class_v<T> << "\n";
}
// Enable a function only for arithmetic types (SFINAE)
template <typename T>
enable_if_t<is_arithmetic_v<T>, T>
clamp(T val, T lo, T hi) {
return max(lo, min(val, hi));
}
int main() {
cout << "int:\n"; typeInfo<int>();
cout << "double:\n"; typeInfo<double>();
cout << "string:\n"; typeInfo<string>();
cout << "int*:\n"; typeInfo<int*>();
cout << clamp(15, 0, 10) << "\n"; // 10
cout << clamp(-5, 0, 10) << "\n"; // 0
cout << clamp(5.5, 0.0, 10.0) << "\n"; // 5.5
return 0;
}C++20 Concepts
Concepts constrain template parameters — better error messages and clearer intent:
#include <iostream>
#include <concepts>
#include <string>
using namespace std;
// Define a concept
template <typename T>
concept Numeric = is_arithmetic_v<T>;
template <typename T>
concept Printable = requires(T t) {
{ cout << t } -> same_as<ostream&>;
};
// Constrained function template
template <Numeric T>
T square(T x) { return x * x; }
// Constrained with 'requires'
template <typename T>
requires Printable<T> && (!is_pointer_v<T>)
void printValue(const T& val) {
cout << "Value: " << val << "\n";
}
// Abbreviated function template (C++20)
auto multiply(Numeric auto a, Numeric auto b) {
return a * b;
}
int main() {
cout << square(5) << "\n"; // 25
cout << square(3.14) << "\n"; // 9.8596
// square("hello"); // Compile error — string is not Numeric
printValue(42);
printValue(3.14);
printValue(string("Hello"));
cout << multiply(3, 4) << "\n"; // 12
cout << multiply(2.5, 4.0) << "\n"; // 10
return 0;
}Practical Example: Generic Matrix
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;
template <typename T>
class Matrix {
int rows, cols;
vector<vector<T>> data;
public:
Matrix(int r, int c, T init = T{})
: rows(r), cols(c), data(r, vector<T>(c, init)) {}
T& operator()(int r, int c) { return data[r][c]; }
const T& operator()(int r, int c) const { return data[r][c]; }
Matrix operator+(const Matrix& rhs) const {
Matrix result(rows, cols);
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
result(r, c) = data[r][c] + rhs.data[r][c];
return result;
}
Matrix operator*(const Matrix& rhs) const {
Matrix result(rows, rhs.cols);
for (int r = 0; r < rows; r++)
for (int c = 0; c < rhs.cols; c++)
for (int k = 0; k < cols; k++)
result(r, c) += data[r][k] * rhs.data[k][c];
return result;
}
// Scalar multiplication
Matrix operator*(T scalar) const {
Matrix result = *this;
for (auto& row : result.data)
for (auto& val : row)
val *= scalar;
return result;
}
void print() const {
for (int r = 0; r < rows; r++) {
cout << "[ ";
for (int c = 0; c < cols; c++) {
cout << setw(6) << fixed << setprecision(1) << data[r][c];
}
cout << " ]\n";
}
}
};
int main() {
Matrix<double> A(2, 2);
A(0,0) = 1; A(0,1) = 2;
A(1,0) = 3; A(1,1) = 4;
Matrix<double> B(2, 2);
B(0,0) = 5; B(0,1) = 6;
B(1,0) = 7; B(1,1) = 8;
cout << "A * B:\n";
(A * B).print();
// [ 19.0 22.0 ]
// [ 43.0 50.0 ]
cout << "A * 2:\n";
(A * 2.0).print();
// [ 2.0 4.0 ]
// [ 6.0 8.0 ]
// Also works for integers!
Matrix<int> C(2, 2, 0);
C(0,0) = 1; C(0,1) = 0;
C(1,0) = 0; C(1,1) = 1;
// Identity matrix
return 0;
}Summary
- Function templates are instantiated for each unique set of argument types — zero runtime overhead
- Class templates parameterise entire classes — the basis of all STL containers
- Template specialisation provides custom implementations for specific types or type categories
- Variadic templates accept any number of type arguments — used for
tuple,make_unique, andprintf-style utilities - C++20 concepts constrain template parameters with readable, compiler-enforced requirements
<type_traits>provides compile-time type information:is_integral_v<T>,is_pointer_v<T>, etc.- Templates are resolved at compile time — template errors can produce long, cryptic messages; concepts improve this dramatically
Next, we'll learn about exception handling — how C++ reports and recovers from errors at runtime.