Templates

C++ · Advanced · 18 min

Templates power the whole standard library; reading and writing them is essential for any serious C++ work.

A template is a pattern from which the compiler generates code for each set of arguments you use. template <typename T> T max_of(T a, T b) works for any T supporting <; calling max_of(3, 7) instantiates max_of<int>. There's no runtime cost: each instantiation is ordinary, fully optimised code.

  • Function templates usually deduce their arguments from the call. Deduction needs consistent types, so max_of(2, 2.5) fails (is T int or double?); write max_of<double>(2, 2.5).
  • Class templates like std::vector<T> parameterise a whole type. Since C++17, CTAD (class template argument deduction) deduces the arguments from the constructor: std::vector v{1, 2, 3}; and std::pair p{1, 2.5};. C++20 extends CTAD to aggregates.
  • Non-type parameters pass compile-time values: template <typename T, std::size_t N> class Ring. C++20 allows floating-point and simple class types here as well.
  • Variadic templates take any number of arguments, template <typename... Ts>, and fold expressions combine them: (xs + ... + 0).

Because the compiler needs the full definition to instantiate it, template code normally lives entirely in headers.

A template parameter written T&& in a deduced context is a forwarding reference, not an rvalue reference. It binds to anything, and std::forward<T>(arg) passes the argument on as an lvalue or rvalue, exactly as it came in. That's how emplace_back and make_unique pass constructor arguments through.

You can specialise a template for particular arguments, template <> struct Hash<MyType> { ... };, to give a custom implementation.

The traditional downside is error messages: a type that lacks an operation fails deep inside the template body. Concepts, next, fix that.

Example

#include <array>
#include <iostream>
#include <string>
#include <utility>

template <typename T>
T max_of(T a, T b) { return a < b ? b : a; }        // any T with operator<

template <typename T, std::size_t N>                 // type + non-type parameter
class Ring {
public:
    void push(T v) { data_[head_++ % N] = std::move(v); }
    const T& operator[](std::size_t i) const { return data_[i % N]; }
private:
    std::array<T, N> data_{};
    std::size_t head_ = 0;
};

template <typename... Ts>                             // variadic template
auto sum(Ts... xs) { return (xs + ... + 0); }        // fold expression

int main() {
    std::cout << max_of(3, 7) << ' ' << max_of<double>(2, 2.5) << '\n';     // 7 2.5
    std::cout << max_of(std::string{"pear"}, std::string{"apple"}) << '\n'; // pear
    Ring<int, 3> r;
    for (int i = 1; i <= 5; ++i) r.push(i);           // overwrites the oldest
    std::pair p{1, 2.5};                              // CTAD: std::pair<int, double>
    std::cout << r[0] << ' ' << sum(1, 2, 3.5) << ' ' << p.second << '\n';  // 4 6.5 2.5
}

Remember

  • Templates generate code per argument set at compile time — no runtime overhead
  • Template definitions go in headers
  • In a deduced template <class T> f(T&& x), T&& is a forwarding reference; pass it on with std::forward<T>(x)

Watch out for

  • Declaring a template in a header and defining it in a .cpp file, then getting undefined-reference link errors.

Check yourself

  1. Why does `max_of(2, 2.5)` fail for `template <typename T> T max_of(T a, T b)`?
  2. In `template <typename T> void wrap(T&& arg)`, what is `T&&`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/templates