C++ Primer / 第 16 章 / 模板与泛型编程
学习首页 知识地图 速查表
CHAPTER 16 / 类设计工具

模板与泛型编程

用函数模板、类模板、模板实参推断和可变参数模板表达通用算法。·11 个细分单元·建议分 2-3 次阅读
教材对应印刷页:577-630 页 · 本书主线按 C++11 讲解;遇到更新标准的写法会特别注明。
本章先回答一个问题: 模板把“类型”变成算法的输入。错误通常发生在实例化时,所以要让约束和命名尽量清楚。
下面每个单元包含概念解释、代码、易错点与阅读提示。
零基础读法:先读概念,再预测代码会做什么;然后逐行追踪输入和变量状态,最后不看示例自己复述。遇到陌生词,可以回到第 1-3 章查类型、作用域和对象生命周期。

详细课程

01 · 函数模板

模板描述一套适用于多种类型的代码。编译器在调用处推断 T,再实例化具体版本;错误往往出现在实例化阶段。

template <typename T>
const T& larger(const T& a, const T& b) {
  return a < b ? b : a;
}
容易踩坑:模板不是运行时魔法,它会生成具体类型的函数。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

02 · 类模板与成员模板

类模板可以把类型作为参数,例如 vector。成员函数也可以是模板,模板参数可以来自类本身或调用本次函数。

template <typename T>
class Box {
  T value_;
public: T get() const { return value_; }
};
容易踩坑:把类型参数命名清楚,错误信息会更容易读。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

03 · 模板实参推断

推断会考虑引用、const、数组和函数指针的特殊规则。传值会发生数组到指针的退化,传引用可以保留更多类型信息。

template <typename T>
void inspect(const T& value);
int a[3]{}; inspect(a);
容易踩坑:需要固定类型时可以显式写 template;否则让推断工作通常更通用。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

04 · 重载、特化与约束思维

普通函数、模板和不同模板参数之间会参与重载决议。完全特化可以为特殊类型提供版本,但不要用特化掩盖设计问题。

template <typename T>
void print(const T&);
template <> void print(const char* const&);
容易踩坑:先问模板需要哪些操作;“鸭子类型”要求这些操作存在且语义合理。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

05 · 转发与可变参数模板

参数包可以接收任意数量参数,std::forward 保留参数原本的左值/右值属性,常用于工厂和包装函数。

template <typename... Args>
auto make(Args&&... args) {
  return Widget(std::forward<Args>(args)...);
}
容易踩坑:完美转发很强,也更难读;先用普通重载,确实需要通用包装时再使用。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

06 · 小练习

实现一个模板函数 print_all,接收任意数量参数并用空格分隔输出;再让它支持一个 vector。

template <typename T, typename... Rest>
void print_all(const T& first, const Rest&... rest);
参考思路:递归展开时要准备终止分支,C++17 以后也可以用折叠表达式。
展开参考代码
template <typename T, typename... Rest>
void print_all(const T& first, const Rest&... rest);

07 · 模板的声明与定义位置

模板必须在实例化处看得到定义,通常把实现写在头文件。普通函数可以只在头文件声明,因为链接器之后再找定义。

// widget.h
 template <typename T> T max_value(T, T) { return ...; }
容易踩坑:把模板实现放进 cpp 会导致调用者实例化时找不到定义。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

08 · 显式实例化与 extern template

显式实例化可以提前生成某个类型的模板,extern template 告诉其他文件不要重复生成,适合大型项目减少编译时间。

extern template class std::vector<int>;
template class Box<int>;
容易踩坑:优化编译时间前先测量,模板可见性优先保证正确。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

09 · 模板参数的类型与非类型

模板参数可以是类型、整数、指针或引用等非类型参数。array 的大小就是典型非类型参数。

template <typename T, std::size_t N>
std::size_t count(const T (&)[N]) { return N; }
容易踩坑:非类型参数必须满足编译期常量要求。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

10 · 尾置返回与 decltype

当返回类型依赖参数时,可以用 auto 加尾置 -> decltype 表达返回类型。decltype((x)) 可能得到引用,括号会改变规则。

template <typename It>
auto first(It it) -> decltype(*it) { return *it; }
容易踩坑:区分 decltype(name) 和 decltype((expression)),它们的结果可能不同。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。

11 · 模板特化

特化为某个具体类型提供不同实现;偏特化为一类类型提供实现,但函数模板不能偏特化,只能重载。

template <typename T> struct Traits;
template <> struct Traits<int> { using type = long; };
容易踩坑:特化应解决真正的语义差异,不要为每个小变化复制一份模板。
自测读法:先遮住代码口述它会做什么;再指出关键变量、边界条件和可能失效的对象。若说不清,回到上面的概念逐句追踪。
REVIEW

本章理解检查

先自己回答,再展开解释。

模板定义为什么通常放在头文件?

实例化发生在调用者所在翻译单元,编译器必须在那里看见模板定义。

完美转发中的 forward 做什么?

按模板推导结果保留实参原先是左值还是右值的属性。

模板实参推断会自动把两个不同类型都转成同一类型吗?

通常不会。若 T 同时从两个参数推断,结果冲突会失败;可显式指定类型或设计不同的模板参数。

类模板和函数模板的特化规则相同吗?

不完全相同。类模板支持偏特化;函数模板不能偏特化,通常用重载实现类似选择。
NEXT

继续学习

完成本章后,建议进入第 17 章;也可以回到首页查看知识地图或打开交互实验。

LEARN BY CHANGING ONE THING

把“我懂了”,变成一次可验证的预测。

实验都在浏览器中运行,无需安装和上传。·使用教学模型,专注理解规则
LAB 01 / SEQUENCE CONTAINER

vector 会怎样长大?

输入一组整数,逐个 push_back,观察 size、capacity 和重新分配次数。

35711
size = 4capacity ≈ 4扩容次数 = 3