Skip to content

8. Templates

What & why

You've been using templates since lesson 1 of the beginner track: std::vector<int>, std::map<std::string, int>, std::sort. A template is a blueprint the compiler uses to generate code for whatever types you supply. You get generic code with zero runtime cost — the generated code is as fast as if you'd written it by hand for each type.

Function templates

template <typename T>
T largest(const T& a, const T& b) {
    return (a < b) ? b : a;
}

largest(3, 7);                                  // T = int
largest(2.5, 1.5);                              // T = double
largest(std::string{"pear"}, std::string{"apple"});  // T = std::string

The compiler deduces T from the arguments and instantiates a version for each distinct T. The requirement on T is implicit: it must support < and copying.

Deduction pitfalls

largest(3, 7.5);            // ERROR: T can't be both int and double
largest<double>(3, 7.5);    // OK: explicit template argument

Abbreviated templates (C++20)

auto parameters make a function a template automatically:

auto largest(const auto& a, const auto& b) {
    return (a < b) ? b : a;
}

Handy for short helpers; the explicit template <typename T> form is clearer when types must match.

Class templates

#include <stdexcept>
#include <vector>

template <typename T>
class Stack {
public:
    void push(T value) { items_.push_back(std::move(value)); }

    T pop() {
        if (items_.empty()) {
            throw std::out_of_range("pop on empty stack");
        }
        T top = std::move(items_.back());
        items_.pop_back();
        return top;
    }

    const T& top() const { return items_.back(); }
    bool empty() const { return items_.empty(); }
    std::size_t size() const { return items_.size(); }

private:
    std::vector<T> items_;
};

Stack<int> numbers;
numbers.push(1);
Stack<std::string> words;
words.push("hello");

Class template argument deduction (CTAD)

std::vector v{1, 2, 3};           // std::vector<int>
std::pair p{"Ada", 36};           // std::pair<const char*, int>
std::array a{1.0, 2.0};           // std::array<double, 2>

Non-type template parameters

Templates can take values, not just types:

template <typename T, std::size_t N>
class RingBuffer {
public:
    void push(const T& v) {
        data_[head_] = v;
        head_ = (head_ + 1) % N;
        if (size_ < N) ++size_;
    }
    std::size_t size() const { return size_; }
private:
    std::array<T, N> data_{};
    std::size_t head_{0};
    std::size_t size_{0};
};

RingBuffer<double, 8> recent;

N is a compile-time constant; RingBuffer<double, 8> and RingBuffer<double, 16> are different types.

Templates live in headers

The compiler needs the template's full definition to instantiate it, in every translation unit that uses it. So templates are defined in headers, not .cpp files. Putting a template's body in a .cpp gives you "undefined reference" linker errors.

Specialization

Provide a custom version for a specific type:

template <typename T>
std::string describe(const T&) { return "something"; }

template <>
std::string describe<bool>(const bool& b) { return b ? "yes" : "no"; }

For functions, plain overloading is usually simpler than specialization:

std::string describe(bool b) { return b ? "yes" : "no"; }

Class templates can be partially specialized:

template <typename T>
struct TypeName { static constexpr const char* value = "unknown"; };

template <typename T>
struct TypeName<T*> { static constexpr const char* value = "pointer"; };   // any pointer

if constexpr: compile-time branching

Choose code paths based on type properties, and the discarded branch isn't even compiled for that type:

#include <string>
#include <type_traits>

template <typename T>
std::string to_text(const T& value) {
    if constexpr (std::is_same_v<T, std::string>) {
        return value;
    } else if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else {
        return "<object>";
    }
}

<type_traits> offers many queries: std::is_integral_v<T>, std::is_pointer_v<T>, std::is_floating_point_v<T>, std::remove_cvref_t<T>, ...

Constraining templates with concepts (preview)

Unconstrained templates produce notoriously long error messages when used with the wrong type. C++20 concepts state requirements up front:

#include <concepts>

template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        a = std::exchange(b, a % b);
    }
    return a;
}

gcd(12, 18);      // OK
gcd(1.5, 2.5);    // clear error: double does not satisfy std::integral

Or with abbreviated syntax: auto gcd(std::integral auto a, std::integral auto b). The advanced track covers concepts in depth.

Type aliases

template <typename T>
using Matrix = std::vector<std::vector<T>>;

Matrix<double> m(3, std::vector<double>(3));

using StringMap = std::unordered_map<std::string, std::string>;

Reading template errors

Template errors can run to hundreds of lines. Strategy:

  1. Find the first error: line.
  2. Look for "required from here" — that's the line in your code that triggered it.
  3. Look for the unmet requirement: "no match for operator<", "no member named ...".
  4. Concepts make this dramatically better — prefer constrained templates.

Common mistakes

  • Defining templates in a .cpp → linker errors.
  • Deduction conflicts — largest(1, 2.0).
  • Forgetting typename for dependent types: typename T::value_type x;.
  • Code bloat — every instantiation is separate machine code. Usually fine; occasionally matters in embedded work.
  • Over-generalizing — a template used with exactly one type is just a harder-to-read function.

Exercises

  1. Write template <typename T> void print_all(const std::vector<T>&), then generalize it to work with any container using const auto&.
  2. Complete Stack<T> with peek() returning std::optional<T> and a constructor from std::initializer_list<T>.
  3. Write template <typename T, std::size_t N> constexpr std::size_t array_size(const T (&)[N]) and use it on a C array.
  4. Finish RingBuffer<T, N> with iteration in oldest-to-newest order and a static_assert(N > 0).
  5. Write to_text using if constexpr to handle strings, numbers, bool, and any type with a .to_string() member (use a requires expression: if constexpr (requires { value.to_string(); })).
  6. Write a Pair<K, V> class template and use CTAD to create one without spelling out the types (add a deduction guide if needed).

Recap

  • Templates are compile-time blueprints; the compiler instantiates code per type, with no runtime cost.
  • Function templates deduce T from arguments; auto parameters create abbreviated templates.
  • Class templates + CTAD: std::vector v{1, 2, 3}.
  • Non-type parameters (std::size_t N) bake values into types.
  • Templates must be defined in headers.
  • if constexpr + <type_traits> for type-dependent code; concepts to constrain templates.

Next: STL algorithms & lambdas →