Phase 3 · Generic Programming & the Standard LibraryModule 14~58 min read

Templates & Concepts

Write reusable, type-safe generic code with function and class templates, deduction, parameter packs, and semantic constraints.

What you'll learn

Templates let one algorithm or type work across many concrete types without giving up static checking. Concepts make the required operations part of the interface, improving overload selection, diagnostics, and the meaning of generic code.

By the end, you'll be able to:

  • Define function and class templates with type and non-type parameters
  • Explain deduction, instantiation, overloads, and specialization
  • Expand parameter packs with fold expressions
  • Constrain generic interfaces with standard and custom concepts

Function templates

A function template is a recipe for a family of functions. When a call supplies arguments, template argument deduction determines types and the compiler instantiates a suitable specialization. The body must be valid for that specialization, not for every imaginable type.

maximum.cpp
#include <iostream>
#include <string>

template <typename T>
const T& maximum(const T& left, const T& right) {
    return left < right ? right : left;
}

int main() {
    std::cout << maximum(4, 9) << '\n';
    std::string first{"Ada"};
    std::string second{"Bjarne"};
    std::cout << maximum(first, second) << '\n';
}

Watch out

Returning a reference is safe here only because both arguments must outlive the use of the result. A generic interface must document lifetime relationships as carefully as type requirements.

Class and non-type parameters

A class template generates types. Type parameters select contained values or policies; non-type parameters represent compile-time values such as a capacity. Different arguments produce different types, so FixedBuffer<int, 8> andFixedBuffer<int, 16> are not interchangeable.

Parameter kindExampleRepresents
Typetypename TA type chosen by the caller or deduction
Non-typestd::size_t NA compile-time value
Templatetemplate<class> class CAnother template
Packtypename... TsZero or more template arguments
fixed_buffer.cpp
#include <array>
#include <cstddef>
#include <stdexcept>

template <typename T, std::size_t Capacity>
class FixedBuffer {
public:
    void push(const T& value) {
        if (size_ == Capacity) throw std::length_error{"buffer full"};
        data_[size_++] = value;
    }

    const T& operator[](std::size_t index) const { return data_.at(index); }
    std::size_t size() const { return size_; }

private:
    std::array<T, Capacity> data_{};
    std::size_t size_{};
};

Deduction and overload resolution

Deduction uses argument types but does not generally apply conversions to make unrelated template arguments agree. Explicit template arguments can fill what cannot be deduced. Ordinary overload resolution then compares viable function templates and non-template functions.

deduction.cpp
#include <iostream>
#include <type_traits>

template <typename Left, typename Right>
auto add(Left left, Right right) {
    return left + right;
}

int main() {
    auto result{add(2, 3.5)};
    static_assert(std::is_same_v<decltype(result), double>);
    std::cout << result << '\n';
}

Note

Prefer overloads and constraints to function-template specialization. Function specializations do not participate in overload resolution like new overloads, which can produce unintuitive selection.

Variadic templates and folds

A parameter pack captures zero or more types or values. A fold expression combines a pack with an operator. The identity and associativity matter: addition needs an initial zero for an empty pack, while stream insertion naturally folds from the output stream.

folds.cpp
#include <iostream>
#include <utility>

template <typename... Values>
auto sum(Values... values) {
    return (0 + ... + values);
}

template <typename... Values>
void print_line(Values&&... values) {
    (std::cout << ... << std::forward<Values>(values)) << '\n';
}

int main() {
    std::cout << sum(3, 4, 5) << '\n';
    print_line("items: ", 3, ", total: ", 12);
}

Concepts and requires

A concept is a compile-time predicate over template arguments. Standard concepts such asstd::integral, std::ranges::range, andstd::totally_ordered describe reusable semantic categories. Constraints can appear before a declaration, after its parameter list, or directly on an abbreviated parameter.

concepts.cpp
#include <concepts>
#include <iostream>

template <std::integral T>
T greatest_common_divisor(T left, T right) {
    while (right != 0) {
        T remainder{left % right};
        left = right;
        right = remainder;
    }
    return left;
}

int main() {
    std::cout << greatest_common_divisor(84, 30) << '\n';
}

Key idea

A concept should express a meaningful contract, not merely test whether one line happens to compile. Semantic names help callers understand what behavior the algorithm expects.

Custom concepts and organization

A requires-expression checks valid syntax, result types, and nested constraints without executing code. Template definitions usually live in headers because an instantiation site needs to see the full definition. Keep templates small and move non-dependent implementation into ordinary source-file functions when possible.

printable.cpp
#include <concepts>
#include <iostream>
#include <string>

template <typename T>
concept Printable = requires(std::ostream& out, const T& value) {
    { out << value } -> std::same_as<std::ostream&>;
};

template <Printable T>
void print(const T& value) {
    std::cout << value << '\n';
}

int main() { print(std::string{"constrained"}); }
  • Constrain at the public boundary, near the template declaration
  • Name concepts for semantics: sortable, serializable, clock-like
  • Avoid duplicating constraints that the implementation does not need
  • Use static_assert inside a type for a local invariant, not as a substitute for an interface constraint

Recap & quick check

Key takeaways

  • Templates generate type-safe functions and classes from compile-time arguments.
  • Deduction determines template arguments before normal overload selection chooses a candidate.
  • Non-type parameters carry compile-time values; packs carry zero or more arguments.
  • Concepts state requirements at the interface and improve selection and diagnostics.
  • Template definitions generally need to remain visible in headers.

Quick check

1. When is a function-template specialization normally instantiated?

2. What can a non-type template parameter represent?

3. What does typename... Ts declare?

4. What is the best role for a concept?

Next: Module 15 — Standard Containers & Adaptors, where generic contracts meet concrete data structures and complexity guarantees.