泛型编程的核心:先描述算法需要什么能力,再让编译器为满足这些能力的具体类型生成代码。它解决的是“复用代码”和“避免运行时抽象开销”之间的矛盾。
5.1 为什么需要泛型编程
假设要分别求两个
int、两个 double 中的较大值。直接写会产生重复代码:两段代码的算法完全相同,变化的只有类型。函数模板把“算法”从“具体类型”中分离出来:
编译器实际做了什么
模板本身更像一份“代码生成规则”。使用模板时,编译器根据实参推导类型,并实例化所需版本。
对比项 | 面向对象动态多态 | 泛型编程静态多态 |
绑定时机 | 运行期 | 编译期 |
常见机制 | 虚函数、基类指针 | 模板、Concepts |
类型要求 | 继承统一接口 | 支持算法所需操作 |
常见开销 | 可能发生间接调用 | 通常可直接调用并内联 |
泛型编程不是“绝对更快”。更准确地说,它让编译器看到具体类型,从而更容易内联和优化;代价是编译时间增加、错误信息复杂,并可能造成代码体积膨胀。
5.2 函数模板:让算法适配多种类型
类型推导与显式指定
当两个实参类型不一致时,单一模板参数可能无法完成推导:
一种直接的改法是允许两个输入类型不同,并让编译器推导返回类型:
为什么常用 const T&
&:避免复制大型对象。
const:保证函数不会修改输入。
- 返回引用:避免再次复制,但调用者必须确保被引用对象仍然存活。
5.3 类模板:构造泛型容器
类模板常用于“数据结构相同、元素类型不同”的场景:
这里既有类型参数
T,也有非类型参数 N。数组容量在编译期确定,不需要动态分配。模板特化:为特定类型改写实现
特化适合处理确实存在差异的类型;如果只是为了修补一个设计混乱的通用模板,继续加特化通常只会把问题藏得更深。
5.4 函数对象与 Lambda:把行为当作参数
函数对象
重载
operator() 后,对象就可以像函数一样调用,并且能够保存状态:Lambda
Lambda 是编译器帮助生成的匿名函数对象。捕获列表决定它能使用哪些外部变量:
与 STL 算法配合
这一组合体现了泛型编程的典型分工:
算法不需要知道容器存的是哪种具体类型,只需要迭代器和判断函数提供约定的操作。
5.5 进阶工具解决什么问题
工具 | 解决的问题 | 典型用途 |
可变参数模板 | 参数数量不固定 | 日志、工厂函数、元组 |
类型萃取 | 在编译期查询或转换类型 | 判断是否为指针、移除引用 |
Concepts | 明确限制模板参数能力 | 提前给出可读的编译错误 |
模板元编程 | 在编译期完成计算或类型变换 | 类型列表、编译期配置 |
Concepts:把隐含要求写出来
传统模板只有在实例化失败时,才可能报出一长串难懂的错误。C++20 Concepts 可以直接声明类型必须满足的条件:
此时,模板表达的不再是“任何类型都可以”,而是“任何满足
Addable 要求的类型都可以”。这才是泛型约束的准确含义。5.6 泛型编程的思想
泛型编程不是简单地把类型名替换成
T,而是三步:- 找出算法中不变的部分,例如遍历、比较、排序。
- 把变化的部分抽成类型、数值或行为参数。
- 明确参数必须提供的最小能力,例如可比较、可调用、可迭代。
判断是否真正理解模板:不要只问“这里的
T 是什么”,而要问“这段算法对 T 做了哪些操作,因此 T 至少必须具备什么能力”。泛型编程的价值可以压缩成一句话:用编译期约束表达共性,用具体类型保留性能。





