Templates
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?
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}
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:
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}
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.
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}
In twice(21), T is int and x + x is 42; in twice(word), T is std::string and x + x joins the two pieces of text. The same code, and + does a different thing for each type.
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:
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}
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:
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}
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>:
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}
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 >:
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}
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:
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}
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:
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}
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 wrote | What 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.
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.
4 1.5 apple
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}
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.
12.5
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}
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.
1 0 1
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}
What you learned
- When functions differ only in type, write one function template:
template <typename T>, withTwhere the type goes. - The compiler deduces
Tfrom 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, likeconst 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 Boxis used with the type named,Box<int>; aStack<T>can be built on astd::vector<T>. vector,map,unique_ptrandsortare all templates.