Boring C++
Boring C++

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

  1. Write a template function clamp<T>(value, min, max) that returns value clamped to the range [min, max]. Test it with int, double, and char.

  2. Write a template function print_vector(const std::vector<T>& v) that prints all elements separated by spaces. Test with vector<int> and vector<std::string>.

Medium

  1. Implement a generic Pair<T, U> class template with public first and second members, a constructor that initializes both, and operator==. Write a make_pair_boring function template that deduces the types from its arguments.

Stretch

  1. Using C++20 Concepts, write a topN(const std::vector<T>& v, int n) function that returns a sorted std::vector<T> containing the n largest elements. Constrain T to be std::totally_ordered. Handle the case where v.size() < n.

7.8 Common mistakes

  • Defining template functions in .cpp files. The compiler needs the full definition when it instantiates the template. Put template definitions in headers. If you see "undefined reference to maximum<int>", this is why.

  • Confusing typename and class in template parameters. They are interchangeable in this context: template<typename T> and template<class T> mean the same thing. Convention: use typename in 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

Boring C++

Modern C++ Mastery for Programmers Who Prefer Clarity Over Cleverness.

View book →