C++_模板与泛型编程
模板与泛型编程
概念
模板(template)可以理解为:
生成类或函数的蓝图
模板本身不是具体的类或函数,而是告诉编译器:
- 如果用户给我某种类型/某个值
- 我就按这个模板生成对应的类或函数
泛型编程的核心思想
泛型编程强调:
写一份通用代码,适用于多种类型
例如:
vector<int>vector<string>findsort
这些都依赖模板机制。
模板的实例化
当我们真正使用模板时,编译器会把模板“变成”具体版本,这个过程叫:
实例化(instantiation)
例如:
1 | vector<int> v; |
会实例化出 vector<int> 这个具体类。
1 | compare(1, 2); |
会实例化出一个针对 int 的 compare 函数。
函数模板
函数模板是用来生成一组函数的公式。
基本定义
你原文里 template 拼错了,正确写法是:
1 | template <typename T> |
含义:
T是模板参数- 实际调用时,
T会被替换成具体类型
模板参数列表
模板定义一般写成:
1 | template <typename T> |
或:
1 | template <class T> |
这里 typename 和 class 在模板参数列表中含义相同。
函数模板的使用
调用时,编译器通常会自动推断模板实参:
1 | cout << compare(1, 0) << endl; // T 推断为 int |
这会实例化出大致等价于:
1 | int compare(const int&, const int&); |
再例如:
1 | vector<int> v1{1,2,3}, v2{4,5,6}; |
会实例化出:
1 | int compare(const vector<int>&, const vector<int>&); |
前提是 vector<int> 支持 < 和 > 比较。
模板并不是“万能的”
模板能否实例化成功,取决于模板中对类型的要求是否满足。
例如这个 compare 模板要求 T 支持:
><
如果类型 T 不支持这些运算,就会编译失败。
所以复习时记住:
模板只在被使用时才检查具体类型是否满足要求
模板参数
模板参数分为两大类:
- 类型参数
- 非类型参数
类型模板参数
类型模板参数表示一个“类型占位符”。
基本写法
1 | template <typename T> |
或
1 | template <class T> |
多个类型参数
你原文这里也有小错误,正确写法应是:
1 | template <typename T, typename U> |
不能写成:
1 | template <typename T, U> // 错误 |
因为每个类型参数前都要写 typename 或 class。
类型参数的用法
类型参数本质上可以当作“类型名”使用:
- 返回类型
- 形参类型
- 局部变量类型
- 类型转换目标类型
例如:
1 | template <typename T> |
非类型模板参数
非类型模板参数表示的不是“类型”,而是“值”。
基本例子
1 | template <unsigned N> |
或者更常见地写成:
1 | template <unsigned N> |
这里 N 是一个值,而不是类型。
非类型模板参数的要求
非类型模板参数必须是编译期常量,通常要求是常量表达式。
常见可作为非类型模板参数的有:
- 整型常量
- 枚举值
- 指针
- 左值引用
nullptr
复习时重点记一句:
非类型模板实参必须是编译时能确定的常量表达式
为什么有用
因为它可以让模板在编译期根据“值”生成不同版本。
例如数组大小:
1 | template <size_t N> |
inline 和 constexpr 的模板
函数模板也可以是:
inlineconstexpr
写法:
1 | template <typename T> |
注意位置:
template<...>在前inline / constexpr在后- 再写返回类型
模板的声明与定义
模板可以先声明,后定义:
1 | template <typename T> |
然后再定义:
1 | template <typename Type> |
模板参数名可以不同,只要:
- 参数个数相同
- 参数种类相同(类型参数/非类型参数)
即可。
模板代码通常放在头文件中
这是模板最重要的实践规则之一。
原因:
模板只有在实例化时才生成代码
编译器必须看到模板定义,才能实例化
所以通常:
- 模板声明放头文件
- 模板定义也放头文件
不像普通函数那样常放到 .cpp。
类模板
类模板是生成类的蓝图。
基本定义
1 | template <typename T> |
使用类模板
1 | MyClass<int> a(10); |
编译器会分别实例化出不同的类:
MyClass<int>MyClass<string>
每个实例是不同类型
这一点非常重要:
类模板的每个实例都是一个独立类型
例如:
1 | MyClass<int> a; |
a 和 b 类型不同,互不相同。
类模板与函数模板的区别
复习时最容易混的一点:
函数模板通常可自动推断
1 | compare(1, 2); |
编译器能推断 T=int。
类模板通常必须显式写模板实参
1 | MyClass<int> obj; |
因为类模板一般不能像函数模板那样方便地从“构造参数”自动推断
(C++17 有类模板实参推断 CTAD,但基础复习里通常先不展开)。
使用 typename 表示“这是一个类型”
这是模板里非常重要的语法点。
典型场景
当模板参数类型内部还有一个类型成员时,编译器默认不确定它是不是类型。
例如:
1 | template <typename T> |
这里:
1 | T::value_type |
前必须加 typename。
为什么要加 typename
因为在模板中,像 T::value_type 这样的名字依赖于模板参数,
编译器默认无法确定它是:
- 一个类型名
- 还是一个静态成员
所以要显式告诉编译器:
这是一个类型
记忆法
模板中出现“依赖于模板参数的嵌套类型名”时,前面通常要加
typename
默认模板实参
模板参数也可以有默认值。
类模板默认实参
1 | template <class T = int> |
使用:
1 | Numbers<long double> x; |
函数模板默认实参
1 | template <typename T, typename F = less<T>> |
这表示:
- 默认比较器是
less<T> - 默认函数实参是
F()
成员模板
一个类可以有成员函数模板,这叫:
成员模板
注意:
成员模板不能是虚函数
因为虚函数依赖运行时多态,模板依赖编译时实例化,两套机制不同。
普通类中的成员模板
1 | class DebugDelete { |
这里 operator() 是成员模板。
类模板中的成员模板
类模板的成员本身也可以再是模板。
类模板参数和成员模板参数彼此独立。
模板参数推断
函数模板调用时,编译器通常会根据实参推断模板参数。
最基本规则
1 | template <typename T> |
调用:
1 | f(42); |
则推断 T=int。
引用和 const 会影响推断
例如:
1 | template <typename T> |
调用:
1 | int a = 10; |
则 T 推断为 int,参数类型变成 const int&。
推断失败的常见情况
如果模板参数不能从函数实参中唯一确定,就会失败。
例如:
1 | template <typename T> |
调用:
1 | compare(1, 3.14); // 通常推断失败 |
因为一个参数像 int,一个像 double,T 无法统一。
这时可:
- 显式指定模板实参
- 或改写模板参数列表
显式模板实参
1 | compare<int>(1, 2); |
表示手动指定 T=int。
此时允许后续函数参数进行普通类型转换。
模板参数推断、引用折叠、转发
std::move
std::move 的作用不是“移动对象”,而是:
把一个左值显式转换成右值引用,从而允许移动语义发生
例如:
1 | string s1 = "abc"; |
这里 std::move(s1) 表示:
- 允许把
s1的资源“挪走” s1之后仍然有效,但值通常不再指定
std::forward
std::forward 用于:
在模板中保持实参原本的值类别(左值/右值)
它通常和右值引用参数、完美转发一起使用。
典型形式
1 | template <typename T> |
这里:
- 如果传入左值,
forward后仍是左值 - 如果传入右值,
forward后仍是右值
为什么需要它
如果不用 forward,模板里的 arg 一旦有名字,就总是左值。
所以:
std::forward是完美转发的核心工具
可变参数模板
可变参数模板可以接受任意个数、任意类型的模板参数。
这是现代 C++ 非常重要的内容。
参数包
有两种包:
- 模板参数包
- 函数参数包
例如:
1 | template <typename T, typename... Args> |
其中:
Args是模板参数包rest是函数参数包
例子理解
1 | foo(i, s, 42, d); |
则:
T对应第一个参数类型Args...对应后面剩余参数类型
空包也是合法的
1 | foo("hi"); |
这时 Args... 可以为空。
所以参数包表示:
零个或多个参数
sizeof...
可变参数模板中常用:
1 | sizeof...(Args) |
作用:
返回参数包中的元素个数
例如:
1 | template <typename... Args> |
包扩展
对参数包能做的最核心操作就是:
展开(扩展)
写法通常是:
1 | pattern... |
例如:
1 | print(args...); |
表示把 args 包中的每个参数依次展开。
转发参数包
可变参数模板常与 forward 结合,形成“完美转发”:
1 | template <typename T, typename... Args> |
这里:
Args&&...接收任意参数forward<Args>(args)...保持每个参数原有左值/右值属性
这是 emplace、make_shared 一类函数的核心思路。
控制实例化
模板通常在使用时自动实例化。
但在大型工程中,相同模板实例可能在多个源文件中重复生成,增加编译开销。
因此 C++ 提供了:
- 显式实例化声明
- 显式实例化定义
显式实例化声明
1 | extern template class Blob<string>; |
含义:
本文件不要生成这个实例,别处会有定义
显式实例化定义
1 | template int compare(const int&, const int&); |
含义:
在这里生成这个模板实例
规则
- 一个实例可以有多个
extern声明 - 但程序中应只有一个显式实例化定义
复习时知道用途即可:
减少重复实例化,降低编译开销
模板特例化(特化)
有时通用模板对某些类型不合适,
这时可以为特定类型提供“特殊版本”。
这叫:
模板特例化 / 特化(specialization)
为什么需要特化
因为通用版本可能:
- 编译不过
- 效率不高
- 语义不合适
函数模板特化示意
例如通用比较:
1 | template <typename T> |
对于 const char*,如果直接比较,比较的是指针地址,不是字符串内容。
因此可为它提供特化版本。
类模板特化也很常见
比如对某个特定类型做特殊处理。
复习时记一句:
特化 = 给某些特殊类型/值提供专门实现
模板的设计原则
对类型要求尽量少
模板写得越通用越好。
例如:
- 如果只需要
<,就不要额外要求>、== - 如果只需要拷贝,就不要额外要求移动或默认构造
错误通常在实例化时暴露
模板定义时未必报错,
但某个具体类型一用,才会发现问题。
所以调模板代码常见现象是:
错误信息在使用点爆发
优先写清晰,再追求复杂技巧
模板语法本来就复杂,复习时先抓:
- 会定义
- 会使用
- 会推断
- 知道
forward/ 可变参数模板的核心用途
常见易错点总结
template 不能拼错
不是 templete
模板参数列表不能为空
必须写至少一个模板参数。
每个类型参数前都要写 typename 或 class
1 | template <typename T, typename U> // 对 |
模板通常放在头文件中
因为实例化时必须看到定义。
类模板的不同实例是不同类型
1 | vector<int>` 和 `vector<string>` |
不是同一种类型。
依赖类型名前通常要加 typename
1 | typename T::value_type |
成员模板不能是虚函数
函数模板不一定总能推断成功
类型不统一时常会失败。
非类型模板参数必须是编译期常量
std::move 不移动,只是转换为右值引用
真正是否移动,取决于后续是否调用移动构造/移动赋值。
std::forward 用于保留值类别
常和 T&& 及可变参数模板一起出现。
结论速记
模板是什么
模板是生成类或函数的蓝图
实例化是什么
编译器把模板变成具体类/具体函数的过程
模板参数有什么
- 类型参数
- 非类型参数
typename 的两种常见作用
- 声明类型模板参数
1 | template <typename T> |
- 说明依赖名是类型
1 | typename T::value_type |
可变参数模板是什么
能接受任意个参数的模板
move 和 forward
move:把左值转成右值引用forward:按原值类别转发参数
特化是什么
给特殊类型/值单独提供实现
总结
模板让我们写出与类型无关的通用代码,编译器在使用时进行实例化;可变参数模板、move/forward 和特化则让模板更灵活、更高效。




