Kodewick
第 25 课,共 30 课目录

模板

第 25 课,共 30 课 · 第 6 部分 · 继承和模板 · 约 15 分钟

第 9 课学过重载:同一个名字,参数类型不同,就写几个函数。可要是几个函数的函数体一模一样,只有类型不同呢?

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}
输出
8
2.5
pear

三个函数,除了类型,一个字都不差。要是发现比较写错了,得改三处;明天要比 char,还得再抄一遍。模板就是让你只写一份,把类型空出来,用到时再填。

函数模板

把上面三个函数换成一个函数模板:

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}
输出
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}

T 是怎么定下来的

编译器从实参推出 T,这叫模板实参推导。max_of(T a, T b) 的两个参数都是 T,所以两个实参必须推出同一个类型。要是一个整数、一个小数:

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}
输出
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:在函数名后面加尖括号。写明了就不用推导,实参照普通函数的规矩转换:

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}
输出
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:

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 << "步数:" << 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。拿一个不会 > 的结构体试试:

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{"红茶", 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 能装一样东西,装什么类型,建的时候说:

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{"生日快乐"};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> 存东西,只露出几个成员函数:

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("首页");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。只给一种类型用的东西,写普通函数就好,报错也短得多。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

什么类型都能用的 min_of

min_of 只能比 int,下面两行用不了。只写一个函数:把它改成函数模板,让程序显示下面三行。

要显示
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:整数和小数比大小

这段代码编译不过。不要改 max_of,也不要改 12 和 12.5,让程序显示 12.5。

要显示
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

在不在范围里

Range 只能装 int,体温要用 double。把它改成类模板,让程序显示 1、0、1 三行。

要显示
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 几个函数只差类型时,写一个函数模板: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 都是模板。