模板
第 9 课学过重载:同一个名字,参数类型不同,就写几个函数。可要是几个函数的函数体一模一样,只有类型不同呢?
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
三个函数,除了类型,一个字都不差。要是发现比较写错了,得改三处;明天要比 char,还得再抄一遍。模板就是让你只写一份,把类型空出来,用到时再填。
函数模板
把上面三个函数换成一个函数模板:
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> 说:下面这个函数里的 T 是一个还没定的类型,叫模板参数。函数体照常写,凡是要写类型的地方写 T。(template <class T> 意思完全一样,两种写法你都会看到。)
调用 max_of(3, 8) 时,编译器看到两个实参都是 int,就定下 T 是 int,照着模板写出一个真正的 int max_of(int a, int b)。max_of(2.5, 1.5) 再写出一个 double 的,max_of(first, second) 再写出一个 std::string 的(字符串按字母顺序比,pear 大)。
点单步:三次都进了 max_of,可第一次 a 和 b 是数,第三次是两段文字。模板本身不是函数,它是编译器写函数用的「模子」;每种类型各有一份。
这段程序显示什么?
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}
twice(21) 里 T 是 int,x + x 是 42;twice(word) 里 T 是 std::string,x + x 是把两段文字接起来。同一份代码,+ 对不同的类型做不同的事。
T 是怎么定下来的
编译器从实参推出 T,这叫模板实参推导。max_of(T a, T b) 的两个参数都是 T,所以两个实参必须推出同一个类型。要是一个整数、一个小数:
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)':找不到能用的 max_of。原因是从 3 推出 int,从 2.5 推出 double,两个对不上,T 定不下来。(在自己电脑上,g++ 还会接着显示几行 note:,其中一行就是 deduced conflicting types for parameter 'T' ('int' and 'double')。)普通函数会把 3 自动转成 3.0,推导的时候不会。
办法是自己写明 T:在函数名后面加尖括号。写明了就不用推导,实参照普通函数的规矩转换:
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> 把 3 变成 3.0,答案 3;max_of<int> 把 2.5 变成 2,答案也是 3,可它悄悄丢了小数。写明类型时,想好你要的是哪一种。
注意: 别把字符串常量直接交给 max_of:max_of("pear", "apple") 推出的 T 是 const char*,比的是两个地址,不是文字。要比文字,先放进 std::string。
参数是 vector<T>
T 不一定单独出现。参数可以是 const std::vector<T>&,编译器从 std::vector<int> 里推出 T 是 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 << "步数:" << sum_of(steps) << '\n';17 std::cout << "金额:" << sum_of(prices) << '\n';18 return 0;19}
步数:18800 金额:23.75
T total = 0; 在 int 版里是 0,在 double 版里是 0.0。返回值也是 T,所以整数加起来还是整数,小数加起来还是小数。
模板对 T 的要求
模板什么类型都能填吗?不是。函数体里对 T 做了什么,填进去的类型就得会什么:max_of 用了 >,sum_of 用了 += 和 = 0。拿一个不会 > 的结构体试试:
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{"红茶", 12};19 Item 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) {
| ~~^~~报错分两层读。第一行 In instantiation of 'T max_of(T, T) [with T = Item]':编译器正在照模板写 T 是 Item 的那一份,这一份是 main 里的调用要的(在自己电脑上,g++ 还会显示一行 required from here,指向第 20 行)。接着才是 error:,在第 11 行:no match for 'operator>' (operand types are 'Item' and 'Item'),两个 Item 不会比大小。
模板的报错常常这样:错在模板里面,原因在调用的地方。修法也有两种:给 Item 写一个 operator>(第 17 课),或者别把 Item 交给 max_of。
类模板
类也能空出类型。下面的 Box 能装一样东西,装什么类型,建的时候说:
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{"生日快乐"};22 coins.put(coins.get() + 5);23 std::cout << coins.get() << '\n';24 std::cout << card.get() << '\n';25 return 0;26}
30 生日快乐
和函数模板不同,类模板要自己在尖括号里写明类型:Box<int>、Box<std::string>。(C++17 起可以写 Box coins{25}; 让编译器推,C++14 不行。)Box<int> 和 Box<std::string> 是两个不同的类,各有自己的 item_。
点单步:画面上 coins 的类型是 Box<int>,card 的是 Box<string>,里面各有一个 item_,一个是数,一个是文字。
小技巧: 成员函数也可以写在类外面,前面要再写一遍 template <typename T>,类名写成 Box<T>:template <typename T> T Box<T>::get() const { return item_; }。
Stack<T>
再写一个有用的:栈(stack),像一摞盘子,后放上去的先拿走。里面用一个 std::vector<T> 存东西,只露出几个成员函数:
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("首页");28 history.push("搜索");29 history.push("商品详情");30 while (!history.empty()) {31 std::cout << "返回:" << history.top() << '\n';32 history.pop();33 }34 return 0;35}
返回:商品详情 返回:搜索 返回:首页
浏览器的「后退」就是这样:最后打开的页面最先退回去。Stack<T> 里的 std::vector<T> 跟着 T 变:Stack<std::string> 里是 std::vector<std::string>,换成 Stack<int> 就是 std::vector<int>,一个字不用改。
点单步看 history 里的格子:三次 push 加在后面,每次 pop 从后面拿走。空了以后再 top() 或 pop() 是未定义行为(引擎会停下),所以先问 empty()。
你早就在用模板了
回头看看,这门课从第 11 课起就一直在用模板:
| 你写过的 | 是什么 |
|---|---|
std::vector<int> | 类模板 vector,T 是 int |
std::map<std::string, int> | 类模板,两个类型参数 |
std::unique_ptr<Order> | 类模板 |
std::make_unique<Order>("咖啡") | 函数模板,写明了 T |
std::sort(v.begin(), v.end()) | 函数模板,从实参推导 |
所以标准库的容器才什么都能装:写 vector 的人不知道你要放什么,只写了一份 vector<T>。std::max(3, 2.5) 也会碰到和 max_of 一样的推导冲突,修法也一样:std::max<double>(3, 2.5)。
自己写模板的时机:发现几个函数或类只差类型,就把类型换成 T。只给一种类型用的东西,写普通函数就好,报错也短得多。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
什么类型都能用的 min_of
✓min_of 只能比 int,下面两行用不了。只写一个函数:把它改成函数模板,让程序显示下面三行。
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}
修好它:整数和小数比大小
✓这段代码编译不过。不要改 max_of,也不要改 12 和 12.5,让程序显示 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}
在不在范围里
✓Range 只能装 int,体温要用 double。把它改成类模板,让程序显示 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}
这一课学会了
- 几个函数只差类型时,写一个函数模板:
template <typename T>,类型的地方写T。 - 编译器从实参推出
T,每种类型照模板写一份函数;单步时看到的是同一个函数名、不同类型的值。 - 推导出两个不同的类型(
max_of(3, 2.5))是编译错误;写明max_of<double>(3, 2.5)就不推导了。 - 参数可以是
const std::vector<T>&这样带T的类型。 - 函数体里对
T用了什么(>、+=),填进去的类型就得会;报错里的In instantiation of … [with T = Item]说的是哪一份。 - 类模板
template <typename T> class Box,用的时候写明Box<int>;Stack<T>可以建在std::vector<T>上。 vector、map、unique_ptr、sort都是模板。