只写一次,用于所有类型
在上一页中,你用 std::sort 给一个 vector<int> 排了序。但 std::sort 同样能给 vector<string>、vector<double> 或你自己的结构体数组排序,而没有人为每种类型单独写一个 sort。这不是魔法,也不是重载。它是模板:同一段代码,编译器会为你交给它的任何类型重复使用。
没有模板,你就只能为每种类型复制粘贴同样的逻辑。下面是同一个 maximum 函数写了三遍,这正是模板要消灭的重复:
int maximum(int a, int b) { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }
函数体完全相同,只有类型不同。模板让你可以说“这对任何类型 T 都适用”,然后只写一次。
函数模板
在函数前面加上 template <typename T>,并在原本该写具体类型的地方都用 T,这个函数就变成了模板。
注意,你从来没有写过 maximum<int> 或 maximum<double>。编译器会看实参,推断出 T 应该是什么,这就是模板实参推导。你每用一种不同的类型调用它,编译器就会在幕后实例化(生成)一个独立的具体函数。
当推导帮不上忙时,你可以用尖括号显式写出类型:
推导中藏着一个常见的陷阱。因为 T 必须是单一类型,混用不同类型的实参就会出错:
maximum(3, 7.5); // ERROR: is T int or double? The compiler refuses to guess.
你可以显式指定来解决,比如 maximum<double>(3, 7.5),或者给每个参数一个各自的类型参数,这正是我们接下来要做的。
多个类型参数
模板并不限于一个类型。需要几个就列几个,用逗号分隔。参数类型可以各不相同的函数就是这样写的:
当返回类型取决于参数时,就用 auto(C++14 及以后)让编译器自己算出来,它和模板天生搭配:
类模板
模板不只用于函数,整个类也可以做成模板。标准容器正是这样工作的:vector<int>、键值对 map 和 pair<A, B> 都是类模板。数据结构只写一次,就能存放你用来参数化它的任何类型。
下面是一个小小的泛型 Box,它能存放任意类型的一个值:
与函数模板的关键区别在于:使用类模板时,你通常必须在尖括号里给出类型,比如 Box<int>,因为在旧标准中没有可以用来推导的构造函数实参。(C++17 加入了类模板实参推导,所以 Box b(42); 也能用,但显式写出总是安全的,读起来也清楚。)
报错会非常长,原因在这里
这部分会让每个人栽跟头,所以值得说清楚。模板只有在用真实类型实例化时才会被完整检查。你可以写一个用到 < 的模板,它单独存在时编译得好好的,直到你用一个没有 < 的类型实例化它,错误才会出现。
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b; // requires that T supports >
}
struct Point { int x, y; };
// maximum(Point{1,2}, Point{3,4});
// ERROR: no operator > for Point. The message names Point AND
// quotes this whole function, often spanning many lines.
因为编译器会把完整的类型代入模板,并从生成的代码内部报告失败,一个小错误就可能产生一大片提到库内部细节的输出。两条生存技巧:
- 读第一条错误,而不是最后一条。后面的错误通常是第一条引发的连锁反应。
- 在信息中寻找你自己的类型名(这里是
Point)。它会告诉你是哪次实例化出了问题。
真正的解决办法,是确保你的类型支持模板所需的一切操作:对 maximum 来说,就是为 Point 重载 operator>,这会在后面的页面中讲到。现代 C++20 的概念(concepts)可以让这些错误更早出现、也更易读,但底层的代入模型是一样的。
下一节:类
你刚刚在专注于模板的同时,构建了一个 Box 类模板:一个带有私有数据、构造函数和成员函数的类。下一页会放慢脚步,好好讲解类:如何把数据和操作它的函数捆绑在一起,public 和 private 到底控制什么,以及成员函数如何访问对象自身的状态。在真实的 C++ 中,模板和类总是结合在一起使用,所以牢牢掌握类会让编写泛型代码容易得多。
常见问题
C++ 中的模板是什么?
模板是一张蓝图,让你只写一次函数或类,然后由编译器为你用到的每种类型生成一个版本。你写下 template <typename T>,再用 T 代替真实的类型。编译器会据此生成一个具体的版本,这叫做实例化。
C++ 模板中的 typename 和 class 有什么区别?
在模板参数列表中,template <typename T> 和 template <class T> 的含义完全相同。如今一般更推荐 typename,因为它表达得更准确:T 可以是任何类型,而不只是类。选哪个关键字对生成的代码没有任何影响。
为什么 C++ 模板的报错信息这么长?
模板是在用真实类型实例化时才被检查的,而不是在编写时。如果某个类型不支持你用到的操作(比如排序需要的 <),错误就会出现在库代码的深处,并把完整的实例化类型全部写出来,产生好几页输出。先读第一条错误,并在其中寻找你自己的类型名。