C++ from the ground up

Templates: one function, many types

Writing a function once and letting the compiler make the version each type needs — the idea behind the whole standard library, shown with one small function.

Part 11 of 12

Advanced18 min readBack to Courses

The problem a template solves

The largest value in a list of whole numbers and the largest value in a list of text are the same algorithm: start with the first item, then keep the one that compares greater. Writing it twice is duplication, and the two copies drift apart the moment one gets fixed.

A template lets you write it once with a placeholder for the type. template <typename T> introduces that placeholder, and T can then be used anywhere a type is allowed — in the parameter list, in the return type, and in the body.

What the compiler does with it

A template is not code yet. When you call largest(numbers) with a vector of ints, the compiler generates a version with T replaced by int, and it does that again for each new type it sees. Those generated versions are called instantiations, and they happen while the program is compiled, not while it runs.

This is why templates cost nothing at run time and why the error messages are long: the compiler is reporting a problem inside code you wrote once but that now exists several times over, and it names each generated version to say which one failed.

What T has to be able to do

A template states no requirements, so the real ones show up as errors when you instantiate it. The body of largest compares items with > and copies one into a local, so a type used with it must support both. A type that cannot be compared will fail inside the template rather than at the call, which is the part that reads as unfriendly.

Templates can also be given several placeholders, and one can be a whole container rather than a single value. The standard library is built from a few ideas of this shape — std::vector, std::map, std::sort are all templates — which is how one library serves every type you will ever write.

One function, three types

#include <iostream>
#include <string>
#include <vector>

template <typename T>
T largest(const std::vector<T>& items) {
    T best = items[0];
    for (const T& item : items) {
        if (item > best) {
            best = item;
        }
    }
    return best;
}

int main() {
    std::vector<int> numbers = {4, 11, 7};
    std::vector<std::string> words = {"pear", "fig"};

    std::cout << largest(numbers) << '\n';
    std::cout << largest(words) << '\n';
    return 0;
}

The same function for four types

Run it and note that one function produced all four answers. Then add a template that returns the smallest, use it on the numbers and the words, and try calling the largest template on a vector of bool.

Output
Click "Run" to execute your code

You can also write largest<int>(values) to name the type yourself. Writing it is rarely needed, but it makes visible what the compiler was inferring all along.

Where the definition has to live

A template is not compiled until it is used, because the compiler does not yet know what T is. That means the definition has to be visible at the point of the call, not merely declared. In a single file this is automatic; across files it is why template code lives in headers rather than in a source file with a separate declaration.

Deduction reads the argument types: largest(numbers) with a std::vector<int> gives T as int, and largest(prices) gives double. You can also name the type — largest<double>(prices) — which is useful when the argument does not determine it, and necessary when a conversion would otherwise pick a surprising T.

A template can take more than one placeholder, and the placeholders can be used in any combination the signature needs. template <typename Key, typename Value> is how a pair-like function stays independent of both types. The standard containers and algorithms are built from the same few ideas, which is how one library serves every type you will write.

Reading a template error

A template error arrives in two layers. The first says where the template was instantiated — which call, with which type — and the second says what went wrong inside the body. Read the second one first: it names the operation the type could not do, such as no match for operator>.

The first layer tells you which of the generated versions failed, which looks like noise until two types use the same template and only one of them is at fault. A useful habit is to instantiate the template by hand in a small program before using it in a larger one; the error is then about one type and one call, and it is much easier to read.

Three template mistakes: calling largest on an empty vector, whose body reads items[0]; assuming a template accepts every type, when the requirements only appear as errors at instantiation; and putting the definition in a source file other files cannot see. Templates are checked per instantiation, so the error names the type that failed.

Try these next

  • Write template <typename T> T smallest(const std::vector<T>&) and use it on ints and strings.
  • Write a two-parameter template that prints a key and a value.
  • Call a template with an explicit type argument and compare it with the deduced call.

One template that totals, one that pairs

Run it and note that one `total` produced an int, a double and a string. Then add a `showPair` call for a word and its length, and a `total` call on a vector of your own.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

When does the compiler make a version of a template function?