Kodewick
Lesson 25 of 30Contents

Templates

Lesson 25 of 30 · Part 6 · Inheritance and templates · about 15 minutes

Lesson 9 covered overloading: one name, different parameter types, a few functions. But what if those functions have exactly the same body, and only the types differ?

main.cpp
1#include <iostream>2#include <string>3 4int max_of(int a, int b) {5    if (a > b) {6        return a;7    }8    return b;9}10 11double max_of(double a, double b) {12    if (a > b) {13        return a;14    }15    return b;16}17 18std::string max_of(std::string a, std::string b) {19    if (a > b) {20        return a;21    }22    return b;23}24 25int main() {26    std::cout << max_of(3, 8) << '\n';27    std::cout << max_of(2.5, 1.5) << '\n';28    std::cout << max_of(std::string("apple"), std::string("pear")) << '\n';29    return 0;30}
Output
8
2.5
pear

Three functions, identical apart from the types. Find a bug in the comparison and you fix it three times; need to compare char tomorrow and you copy it again. A template lets you write it once, leave the type blank, and fill it in where it's used.

Function templates

Replace the three functions with one function template:

main.cpp
1#include <iostream>2#include <string>3 4template <typename T>5T max_of(T a, T b) {6    if (a > b) {7        return a;8    }9    return b;10}11 12int main() {13    std::cout << max_of(3, 8) << '\n';14    std::cout << max_of(2.5, 1.5) << '\n';15    std::string first = "apple";16    std::string second = "pear";17    std::cout << max_of(first, second) << '\n';18    return 0;19}
Output
8
2.5
pear

template <typename T> says: in the function below, T is a type not decided yet, called a template parameter. Write the body as usual, with T wherever a type goes. (template <class T> means exactly the same; you'll see both.)

When you call max_of(3, 8), the compiler sees two int arguments, settles on T being int, and writes a real int max_of(int a, int b) from the template. max_of(2.5, 1.5) makes it write a double one, and max_of(first, second) a std::string one (strings compare in alphabetical order, so pear is bigger).

Press Step: all three calls go into max_of, but the first time a and b are numbers, and the third time they're text. A template isn't a function itself; it's a mold the compiler uses to write functions, one for each type.

Guess first

What does this program show?

1#include <iostream>2#include <string>3 4template <typename T>5T twice(T x) {6    return x + x;7}8 9int main() {10    std::string word = "ha";11    std::cout << twice(21) << " " << twice(word) << '\n';12    return 0;13}

How T gets decided

The compiler works out T from the arguments; this is called template argument deduction. Both parameters of max_of(T a, T b) are T, so both arguments must give the same type. With one whole number and one decimal:

main.cpp
1#include <iostream>2 3template <typename T>4T max_of(T a, T b) {5    if (a > b) {6        return a;7    }8    return b;9}10 11int main() {12    std::cout << max_of(3, 2.5) << '\n';13    return 0;14}
Output
main.cpp: In function 'int main()':
main.cpp:12:24: error: no matching function for call to 'max_of(int, double)'
   12 |     std::cout << max_of(3, 2.5) << '\n';
      |                  ~~~~~~^~~~~~~~

no matching function for call to 'max_of(int, double)': no usable max_of. The reason: 3 says int, 2.5 says double, they don't agree, and T can't be settled. (On your own computer, g++ goes on with a few note: lines, one of them deduced conflicting types for parameter 'T' ('int' and 'double').) An ordinary function would turn 3 into 3.0 for you; deduction doesn't.

The fix is to name T yourself, in angle brackets after the function name. Then there's no deduction, and the arguments convert just as for an ordinary function:

main.cpp
1#include <iostream>2 3template <typename T>4T max_of(T a, T b) {5    if (a > b) {6        return a;7    }8    return b;9}10 11int main() {12    std::cout << max_of<double>(3, 2.5) << '\n';13    std::cout << max_of<int>(3, 2.5) << '\n';14    return 0;15}
Output
3
3

max_of<double> turns 3 into 3.0, and the answer is 3; max_of<int> turns 2.5 into 2, and the answer is also 3, but it quietly dropped the decimal part. When you name the type, think about which one you want.

Note: Don't hand string literals straight to max_of: max_of("pear", "apple") deduces T as const char*, and compares two addresses, not the text. To compare text, put it in a std::string first.

A vector<T> parameter

T doesn't have to stand alone. A parameter can be const std::vector<T>&, and the compiler finds T is int inside std::vector<int>:

main.cpp
1#include <iostream>2#include <vector>3 4template <typename T>5T sum_of(const std::vector<T>& items) {6    T total = 0;7    for (const T& x : items) {8        total += x;9    }10    return total;11}12 13int main() {14    std::vector<int> steps{4200, 8100, 6500};15    std::vector<double> prices{3.5, 12, 8.25};16    std::cout << "Steps: " << sum_of(steps) << '\n';17    std::cout << "Amount: " << sum_of(prices) << '\n';18    return 0;19}
Output
Steps: 18800
Amount: 23.75

T total = 0; is 0 in the int version and 0.0 in the double version. The return type is T too, so whole numbers add up to a whole number, and decimals to a decimal.

What a template needs from T

Can any type go into a template? No. Whatever the body does with T, the type must support: max_of uses >, and sum_of uses += and = 0. Try a struct that has no >:

main.cpp
1#include <iostream>2#include <string>3 4struct Item {5    std::string name;6    double price;7};8 9template <typename T>10T max_of(T a, T b) {11    if (a > b) {12        return a;13    }14    return b;15}16 17int main() {18    Item tea{"tea", 12};19    Item cake{"cake", 18};20    Item best = max_of(tea, cake);21    std::cout << best.name << '\n';22    return 0;23}
Output
main.cpp: In instantiation of 'T max_of(T, T) [with T = Item]':
main.cpp:11:11: error: no match for 'operator>' (operand types are 'Item' and 'Item')
   11 |     if (a > b) {
      |         ~~^~~

Read this error in two layers. The first line, In instantiation of 'T max_of(T, T) [with T = Item]', means the compiler is writing the copy of the template where T is Item, the copy the call in main asked for (on your own computer, g++ also shows a required from here line pointing at line 20). Then comes the error:, on line 11: no match for 'operator>' (operand types are 'Item' and 'Item'). Two Items can't be compared.

Template errors often look like this: the mistake shows up inside the template, but the cause is at the call. There are two fixes: give Item an operator> (lesson 17), or don't pass Item to max_of.

Class templates

A class can leave a type blank too. This Box holds one thing, and you say what type when you create it:

main.cpp
1#include <iostream>2#include <string>3 4template <typename T>5class Box {6public:7    Box(T item) : item_(item) {}8    T get() const {9        return item_;10    }11    void put(T item) {12        item_ = item;13    }14 15private:16    T item_;17};18 19int main() {20    Box<int> coins{25};21    Box<std::string> card{"Happy birthday"};22    coins.put(coins.get() + 5);23    std::cout << coins.get() << '\n';24    std::cout << card.get() << '\n';25    return 0;26}
Output
30
Happy birthday

Unlike a function template, with a class template you name the type yourself in angle brackets: Box<int>, Box<std::string>. (Since C++17 you can write Box coins{25}; and let the compiler work it out; not in C++14.) Box<int> and Box<std::string> are two different classes, each with its own item_.

Press Step: on screen, coins has type Box<int> and card has type Box<string>, each holding an item_, one a number, one text.

Tip: Member functions can also go outside the class. Write template <typename T> again in front, and the class name as Box<T>: template <typename T> T Box<T>::get() const { return item_; }.

Stack<T>

Now a useful one: a stack, like a pile of plates, where the last one put on is the first taken off. Inside, a std::vector<T> stores things, and only a few member functions are shown to the outside:

main.cpp
1#include <iostream>2#include <string>3#include <vector>4 5template <typename T>6class Stack {7public:8    void push(const T& x) {9        items_.push_back(x);10    }11    void pop() {12        items_.pop_back();13    }14    const T& top() const {15        return items_.back();16    }17    bool empty() const {18        return items_.empty();19    }20 21private:22    std::vector<T> items_;23};24 25int main() {26    Stack<std::string> history;27    history.push("Home");28    history.push("Search");29    history.push("Product page");30    while (!history.empty()) {31        std::cout << "Back to: " << history.top() << '\n';32        history.pop();33    }34    return 0;35}
Output
Back to: Product page
Back to: Search
Back to: Home

A browser's Back button works like this: the last page opened is the first one you go back from. The std::vector<T> inside Stack<T> follows T: in Stack<std::string> it's std::vector<std::string>, and in Stack<int> it would be std::vector<int>, without changing a word.

Press Step and watch the cells in history: each push adds at the back, each pop takes from the back. Calling top() or pop() once it's empty is undefined behavior (the engine stops), so ask empty() first.

You've been using templates all along

Look back: since lesson 11, this course has been using templates all the time:

You wroteWhat it is
std::vector<int>class template vector, T is int
std::map<std::string, int>class template, two type parameters
std::unique_ptr<Order>class template
std::make_unique<Order>("coffee")function template, T named
std::sort(v.begin(), v.end())function template, deduced from the arguments

That's why standard containers can hold anything: whoever wrote vector didn't know what you'd put in it, so they wrote one vector<T>. std::max(3, 2.5) runs into the same deduction conflict as max_of, with the same fix: std::max<double>(3, 2.5).

When to write your own template: when you notice several functions or classes that differ only in type, turn the type into T. For something used with only one type, an ordinary function is fine, and its errors are much shorter.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

A min_of for any type

min_of only compares int, so two of the lines below can't use it. Write only one function: turn it into a function template so the program shows these three lines.

It should print
4
1.5
apple
exercise1.cpp
1#include <iostream>2#include <string>3 4int min_of(int a, int b) {5    if (a < b) {6        return a;7    }8    return b;9}10 11int main() {12    std::string x = "pear";13    std::string y = "apple";14    std::cout << min_of(4, 9) << '\n';15    std::cout << min_of(2.5, 1.5) << '\n';16    std::cout << min_of(x, y) << '\n';17    return 0;18}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: a whole number vs. a decimal

This code doesn't compile. Without changing max_of, 12 or 12.5, make the program show 12.5.

It should print
12.5
exercise2.cpp
1#include <iostream>2 3template <typename T>4T max_of(T a, T b) {5    if (a > b) {6        return a;7    }8    return b;9}10 11int main() {12    std::cout << max_of(12, 12.5) << '\n';13    return 0;14}
Output
Press Run or Ctrl+Enter
Exercise 3

Inside the range

Range only holds int, but body temperature needs double. Turn it into a class template so the program shows three lines: 1, 0, 1.

It should print
1
0
1
exercise3.cpp
1#include <iostream>2 3class Range {4public:5    Range(int low, int high) : low_(low), high_(high) {}6    bool contains(int x) const {7        return x >= low_ && x <= high_;8    }9 10private:11    int low_;12    int high_;13};14 15int main() {16    Range<int> age{18, 60};17    Range<double> normal{36.0, 37.3};18    std::cout << age.contains(25) << '\n';19    std::cout << normal.contains(37.8) << '\n';20    std::cout << normal.contains(36.6) << '\n';21    return 0;22}
Output
Press Run or Ctrl+Enter

What you learned

  • When functions differ only in type, write one function template: template <typename T>, with T where the type goes.
  • The compiler deduces T from the arguments and writes one function per type from the template; stepping shows the same function name with values of different types.
  • Deducing two different types (max_of(3, 2.5)) is a compile error; naming the type, max_of<double>(3, 2.5), skips deduction.
  • A parameter can be a type that contains T, like const std::vector<T>&.
  • Whatever the body does with T (>, +=), the type must support; in the error, In instantiation of … [with T = Item] tells you which copy.
  • A class template template <typename T> class Box is used with the type named, Box<int>; a Stack<T> can be built on a std::vector<T>.
  • vector, map, unique_ptr and sort are all templates.