Templates and Generic Programming
11 min read
Chapter 7: Templates and Generic Programming
Templates are C++'s mechanism for writing code once that works correctly for many types. Without templates, you would write sumInts, sumDoubles, sumLongs as separate functions. With templates, you write sum once and the compiler generates the right version for each type you use it with.
The concept is simple. The details are not always. Template error messages are notoriously long and cryptic — a one-line mistake can produce fifteen lines of compiler output. C++20 Concepts exist specifically to improve this situation, and we use them in this chapter. But even with Concepts, reading template errors is a skill that takes practice.
Do not be discouraged by that. The payoff is substantial: the entire standard library — std::vector, std::sort, std::map, every algorithm — is written with templates. Understanding templates is understanding how C++ is actually built.
7.1 The problem templates solve
Suppose you need a function that returns the larger of two values. Without templates:
#include <iostream>
#include <string>
// Three separate functions — identical logic, different types
int maxInt(int a, int b) { return a > b ? a : b; }
double maxDouble(double a, double b) { return a > b ? a : b; }
std::string maxString(const std::string& a, const std::string& b) {
return a > b ? a : b;
}
int main() {
std::cout << maxInt(3, 7) << "\n";
std::cout << maxDouble(3.14, 2.71) << "\n";
std::cout << maxString("apple", "banana") << "\n";
return 0;
}
The code is identical in all three functions. If you find a bug in the logic, you fix it in three places. If you add a new type, you add a fourth function. Templates eliminate this duplication:
#include <iostream>
#include <string>
template<typename T>
T maximum(const T& a, const T& b) {
return a > b ? a : b;
}
int main() {
std::cout << maximum(3, 7) << "\n"; // int
std::cout << maximum(3.14, 2.71) << "\n"; // double
std::cout << maximum(std::string{"apple"}, std::string{"banana"}) << "\n"; // string
return 0;
}
The template says: "for any type T, maximum does this." The compiler instantiates the template for each concrete type used — it generates maximum<int>, maximum<double>, and maximum<std::string> automatically.
7.2 Function templates
The template<typename T> prefix introduces a template parameter. Inside the function, T can be used anywhere a type can appear.
#include <iostream>
#include <vector>
#include <string>
// Single type parameter
template<typename T>
const T& minimum(const T& a, const T& b) {
return a < b ? a : b;
}
// Two type parameters
template<typename T, typename U>
void printPair(const T& first, const U& second) {
std::cout << "(" << first << ", " << second << ")\n";
}
// Non-type template parameter (a value, not a type)
template<int N>
void printNTimes(const std::string& s) {
for (int i = 0; i < N; ++i) {
std::cout << s << "\n";
}
}
// Template computing sum of a vector
template<typename T>
T vectorSum(const std::vector<T>& v) {
T result{}; // zero-initializes: 0 for numbers, "" for string
for (const auto& x : v) result += x;
return result;
}
int main() {
std::cout << minimum(10, 3) << "\n"; // 3 (int)
std::cout << minimum(3.14, 2.71) << "\n"; // 2.71 (double)
std::cout << minimum('z', 'a') << "\n"; // a (char)
printPair(42, "hello"); // (42, hello)
printPair(3.14, true); // (3.14, 1)
printPair(std::string{"x"}, 99); // (x, 99)
printNTimes<3>("echo");
std::vector<int> ints{1, 2, 3, 4, 5};
std::vector<double> doubles{1.1, 2.2, 3.3};
std::cout << "Sum ints: " << vectorSum(ints) << "\n";
std::cout << "Sum doubles: " << vectorSum(doubles) << "\n";
return 0;
}
Type deduction: in most cases you do not need to write maximum<int>(3, 7) — the compiler deduces T from the arguments. You can always be explicit: maximum<double>(3, 7) forces T = double and converts both arguments.
Where to define templates: template definitions must be visible at the point where they are instantiated. This means they must go in header files (.h or .hpp), not in .cpp files. If you put the definition in a .cpp and only the declaration in the header, the linker will fail with "undefined reference" when another file tries to instantiate it.
7.3 Class templates
Just as functions can be templated, so can classes. Here is a generic stack:
#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>
template<typename T>
class Stack {
public:
void push(const T& value) {
data_.push_back(value);
}
void push(T&& value) {
data_.push_back(std::move(value));
}
T pop() {
if (empty()) throw std::underflow_error{"Stack is empty"};
T value = std::move(data_.back());
data_.pop_back();
return value;
}
const T& top() const {
if (empty()) throw std::underflow_error{"Stack is empty"};
return data_.back();
}
bool empty() const { return data_.empty(); }
size_t size() const { return data_.size(); }
private:
std::vector<T> data_;
};
int main() {
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
std::cout << "Top: " << intStack.top() << "\n"; // 3
std::cout << "Pop: " << intStack.pop() << "\n"; // 3
std::cout << "Pop: " << intStack.pop() << "\n"; // 2
std::cout << "Size: " << intStack.size() << "\n"; // 1
Stack<std::string> strStack;
strStack.push("hello");
strStack.push("world");
std::cout << "Top: " << strStack.top() << "\n"; // world
try {
Stack<int> empty;
empty.pop(); // throws
} catch (const std::underflow_error& e) {
std::cerr << "Caught: " << e.what() << "\n";
}
return 0;
}
Stack<int> and Stack<std::string> are different types — the compiler generates separate code for each. They share no runtime overhead between them.
Class template argument deduction (CTAD, C++17) lets the compiler sometimes deduce template arguments from constructor arguments:
Stack stack{42}; // Stack<int> if the class has a deduction guide or a suitable constructor
For complex class templates, you often still need to write the type explicitly: Stack<int>.
7.4 Template specialization
You can provide a different implementation for a specific type:
#include <iostream>
#include <string>
#include <cstring>
// Primary template: works for most types
template<typename T>
void printType(const T& value) {
std::cout << "[Unknown type]: " << value << "\n";
}
// Full specialization for int
template<>
void printType<int>(const int& value) {
std::cout << "[int]: " << value << "\n";
}
// Full specialization for double
template<>
void printType<double>(const double& value) {
std::cout << "[double]: " << value << "\n";
}
// Full specialization for std::string
template<>
void printType<std::string>(const std::string& value) {
std::cout << "[string, length=" << value.size() << "]: " << value << "\n";
}
// Specialization can also provide completely different algorithms:
// Primary: general comparison
template<typename T>
bool safeEquals(const T& a, const T& b) {
return a == b;
}
// Specialization for C-strings: use strcmp instead of ==
template<>
bool safeEquals<const char*>(const char* const& a, const char* const& b) {
return std::strcmp(a, b) == 0;
}
int main() {
printType(42);
printType(3.14);
printType(std::string{"hello"});
printType(true); // uses primary template
std::cout << std::boolalpha;
std::cout << safeEquals(42, 42) << "\n"; // true
std::cout << safeEquals("hello", "hello") << "\n"; // true (uses strcmp)
std::cout << safeEquals(3.14, 2.71) << "\n"; // false
return 0;
}
Partial specialization works for class templates (not function templates) and lets you specialize based on a pattern, not just a specific type. For example, specializing for any pointer type T* or for std::vector<T>.
For function templates, when you need different behavior for a specific type, prefer overloading over full specialization. Overloads interact with the rest of the overload resolution rules in a more predictable way.
7.5 Concepts (C++20)
Before Concepts, if you instantiated a template with a type that did not support the required operations, the error messages were notoriously bad:
template<typename T>
T sum(const std::vector<T>& v) {
T result{};
for (const auto& x : v) result += x;
return result;
}
// sum(std::vector<std::fstream>{...})
// → 50 lines of incomprehensible error text about operator+= not being defined
Concepts (C++20) let you state what operations a type must support, and generate a clear, actionable error if the constraint is not satisfied:
#include <iostream>
#include <concepts>
#include <vector>
#include <string>
// Concept: T must support +=, be default-constructible, and be summable
template<typename T>
concept Summable = requires(T a, T b) {
{ a += b }; // must support +=
T{}; // must be default-constructible
};
template<Summable T>
T sum(const std::vector<T>& v) {
T result{};
for (const auto& x : v) result += x;
return result;
}
// Standard library concepts:
template<std::integral T> // T must be an integer type
T factorial(T n) {
T result{1};
for (T i = 2; i <= n; ++i) result *= i;
return result;
}
template<std::floating_point T> // T must be a floating-point type
T hypotenuse(T a, T b) {
return std::sqrt(a * a + b * b);
}
int main() {
std::vector<int> ints{1, 2, 3, 4, 5};
std::vector<double> doubles{1.1, 2.2, 3.3};
std::cout << "Sum ints: " << sum(ints) << "\n";
std::cout << "Sum doubles: " << sum(doubles) << "\n";
// sum(std::vector<bool>{true, false});
// ^ clear error: bool doesn't satisfy Summable (bool has no +=)
std::cout << "5! = " << factorial(5) << "\n";
std::cout << "10! = " << factorial(10LL) << "\n";
std::cout << "Hypotenuse 3,4: " << hypotenuse(3.0, 4.0) << "\n";
return 0;
}
Standard library concepts in <concepts>:
| Concept | Meaning |
|---|---|
std::integral |
Integer types (int, long, char, ...) |
std::floating_point |
float, double, long double |
std::signed_integral |
Signed integers |
std::unsigned_integral |
Unsigned integers |
std::copyable |
Can be copied |
std::movable |
Can be moved |
std::equality_comparable |
Supports == and != |
std::totally_ordered |
Supports <, >, <=, >= |
std::invocable<F, Args...> |
F can be called with Args |
Compile with -std=c++20 for Concepts.
7.6 Variadic templates (brief introduction)
Variadic templates accept any number of type parameters. They are used internally by the standard library for std::make_unique, std::tuple, std::visit, and similar facilities.
#include <iostream>
#include <string>
// Base case: no more arguments
void printAll() {
std::cout << "\n";
}
// Recursive case: process first argument, recurse on the rest
template<typename First, typename... Rest>
void printAll(const First& first, const Rest&... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
printAll(rest...);
}
// Fold expression (C++17): more concise than recursion
template<typename... Args>
void printLine(const Args&... args) {
((std::cout << args << " "), ...);
std::cout << "\n";
}
// Variadic sum using fold expression
template<typename... Args>
auto foldSum(const Args&... args) {
return (args + ...); // fold over +
}
int main() {
printAll(1, 2.5, "hello", true);
printLine(42, "world", 3.14, 'x');
std::cout << "Sum: " << foldSum(1, 2, 3, 4, 5) << "\n"; // 15
std::cout << "Sum: " << foldSum(1.1, 2.2, 3.3) << "\n"; // 6.6
return 0;
}
if constexpr (C++17) is a compile-time if — the branch that is false is not compiled at all. It is essential for recursive templates where the base case has a different structure from the recursive case.
Variadic templates are an advanced topic. You will use them every time you call std::make_unique or std::vector::emplace_back, but writing your own variadic templates from scratch is relatively rare outside of library code.
7.7 Exercises
Easy
-
Write a template function
clamp<T>(value, min, max)that returnsvalueclamped to the range[min, max]. Test it withint,double, andchar. -
Write a template function
print_vector(const std::vector<T>& v)that prints all elements separated by spaces. Test withvector<int>andvector<std::string>.
Medium
- Implement a generic
Pair<T, U>class template with publicfirstandsecondmembers, a constructor that initializes both, andoperator==. Write amake_pair_boringfunction template that deduces the types from its arguments.
Stretch
- Using C++20 Concepts, write a
topN(const std::vector<T>& v, int n)function that returns a sortedstd::vector<T>containing thenlargest elements. ConstrainTto bestd::totally_ordered. Handle the case wherev.size() < n.
7.8 Common mistakes
-
Defining template functions in
.cppfiles. The compiler needs the full definition when it instantiates the template. Put template definitions in headers. If you see "undefined reference tomaximum<int>", this is why. -
Confusing
typenameandclassin template parameters. They are interchangeable in this context:template<typename T>andtemplate<class T>mean the same thing. Convention: usetypenamein modern code. -
Instantiation explosion. Every distinct set of template arguments generates separate compiled code.
std::vector<int>,std::vector<double>,std::vector<std::string>are three separate types with three separate compiled implementations. This is usually fine, but can bloat binary size if you instantiate with many types. -
Expecting clear error messages from unconstrained templates. Without Concepts, a type mismatch inside a template body produces errors at the instantiation site, buried inside template instantiation traces. Use Concepts (-std=c++20) to get clear errors at the constraint check.
-
Trying to overload function templates on return type alone. Just like regular functions, template functions cannot be overloaded on return type.
-
Not understanding that template specialization and overloading interact. Full specializations of function templates are chosen after overload resolution selects the primary template — not during overload resolution. This can produce unexpected behavior. Prefer overloads to specializations for function templates.
-
Circular dependencies with templates in headers. Including header A from B and B from A, where both contain templates that reference types in the other, creates a cycle. Reorganize so the common types are in a third header.
7.9 Summary
Templates let you write code once that works for many types. The compiler generates a separate instantiation for each concrete type used.
Function templates deduce their type arguments from the call-site arguments in most cases. Class templates require explicit type arguments: Stack<int>.
Template specialization provides a different implementation for specific types. Prefer overloading for function templates; partial and full specialization are most useful for class templates.
Concepts (C++20) constrain template parameters and produce human-readable error messages when constraints are not satisfied. Standard library concepts in <concepts> cover the most common cases.
Variadic templates accept any number of arguments and are the mechanism behind std::make_unique, std::tuple, and similar facilities. You use them constantly without always knowing it.
In Chapter 8 we turn to the standard library — the containers and algorithms you will reach for every day.
All code for this chapter is in the GitHub repository:
<TODO: book code repository URL>/tree/main/chapter07