Menu

C++ 模板详解:泛型函数与类模板

用 C++ 模板只写一次代码,就让它适用于所有类型:函数模板、类模板、类型推导,以及它们带来的令人困惑的编译错误。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

只写一次,用于所有类型

在上一页中,你用 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++ 模板的报错信息这么长?

模板是在用真实类型实例化时才被检查的,而不是在编写时。如果某个类型不支持你用到的操作(比如排序需要的 <),错误就会出现在库代码的深处,并把完整的实例化类型全部写出来,产生好几页输出。先读第一条错误,并在其中寻找你自己的类型名。

Coddy 编程语言插图

和 Coddy 一起学编程

开始