模板与泛型编程
掌握C++模板编程——函数模板/类模板/模板实例化/模板参数推导/可变参数模板——写出类型无关的泛型代码(类设计者工具篇收官)
学习目标
- 能解释函数模板和类模板的工作机制——编译器如何在编译期从模板生成具体类型的代码,隐式和显式实例化的区别,以及类型参数推导的规则
- 能自己写出泛型的数据结构(
Stack<T>、Pair<T,U>)和泛型算法(find、compare),正确处理类外成员函数定义的template前缀和ClassName<T>::作用域限定 - 能回答:写一个
Stack类模板,支持push、pop、top、empty——写出头文件中的完整模板声明(类体 + 类外成员函数定义),并说明Stack<int>和Stack<string>这两个"类"之间是什么关系
机制总览
模板与泛型编程:机制路径
- 1
直觉:为什么需要模板
你写了一个排序函数——接受 int 数组排序。老板说"同样的排序逻辑, double 数组也要"。你复制粘贴,改了类型。老板又说 string 数组也要、 long 也要——你复制了四次几乎一模一样的代码,只改了类型名。每次代码逻辑出现 bug,你要改四个地方。这显然不是一个好办法。
- 2
官方 Chapter 16 的完整模板解析链
模板调用不是简单的文本替换。编译器先形成候选,推断模板实参,进行重载决议与模板偏序,再把选中模式代入并检查实例语义。某个类型能写进模板参数,不代表模板体里的全部操作对它都有效。
- 3
函数模板:从具体函数到类型无关的公式
你写 int max(int a, int b) return a b ?
章级决策实验
模板与泛型编程:机制与证据
切换《模板与泛型编程》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 直觉:为什么需要模板
你写了一个排序函数——接受 int 数组排序。老板说"同样的排序逻辑, double 数组也要"。你复制粘贴,改了类型。老板又说 string 数组也要、 long 也要——你复制了四次几乎一模一样的代码,只改了类型名。每次代码逻辑出现 bug,你要改四个地方。这显然不是一个好办法。
可核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:为什么需要模板」的契约。
学完《模板与泛型编程》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
模板与泛型编程:失效与核验
直觉:为什么需要模板
典型失效
若把「直觉:为什么需要模板」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:为什么需要模板」的契约。
官方 Chapter 16 的完整模板解析链
典型失效
若把「官方 Chapter 16 的完整模板解析链」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「官方 Chapter 16 的完整模板解析链」的契约。
函数模板:从具体函数到类型无关的公式
典型失效
若把「函数模板:从具体函数到类型无关的公式」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。
核验证据
保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「函数模板:从具体函数到类型无关的公式」的契约。
直觉:为什么需要模板
你写了一个排序函数——接受 int 数组排序。老板说"同样的排序逻辑,double 数组也要"。你复制粘贴,改了类型。老板又说 string 数组也要、long 也要——你复制了四次几乎一模一样的代码,只改了类型名。每次代码逻辑出现 bug,你要改四个地方。这显然不是一个好办法。
想象工厂里有一个模具——你向模具里注入不同的材料,就能批量生产不同型号的产品。模具本身定义的是"形状"而不是"材料"。模板就是 C++ 的"代码模具"——你用 T 这个占位符代替具体类型,写下函数的逻辑一次。当调用时,编译器自动把 T 替换成实际类型——int、double、string——帮你批量生成不同类型的版本。
这一章解决什么问题? 教你用模板写出"类型无关"的代码——一套逻辑,所有类型通用。你不再需要为 int 写一遍、为 double 写一遍、为 string 写一遍——写一次就够了。没有模板,你的代码库会被大量"只有类型不同"的重复代码撑爆——更可怕的是每份副本都可能藏着彼此不同步的 bug。模板是 C++ 标准库的基石——从 vector<int> 到 sort(v.begin(), v.end())——它们全都是用模板实现的。
官方 Chapter 16 的完整模板解析链
模板调用不是简单的文本替换。编译器先形成候选,推断模板实参,进行重载决议与模板偏序,再把选中模式代入并检查实例语义。某个类型能写进模板参数,不代表模板体里的全部操作对它都有效。
模板实参推断、引用折叠与数组
↡编译器根据函数实参和函数模板形参模式推导模板参数的过程。推断发生在隐式转换之前,引用、顶层 const、数组和函数类型各有调整规则。 不会为了让多个实参“看起来兼容”而先做普通算术转换,所以 max(3, 4.5) 不能为同一个 T 同时推断 int 和 double。按值形参会忽略顶层 const,并把数组/函数实参调整为指针;引用形参保留更多类型信息,数组引用甚至能推断长度。
template<typename T, std::size_t N>
constexpr std::size_t array_size(const T (&)[N]) { return N; }
int values[42];
static_assert(array_size(values) == 42, "size mismatch");↡形如模板参数 T 的右值引用形参,且 T 需要由调用推断。传左值时 T 推断为左值引用,传右值时 T 推断为非引用类型;它不是普通右值引用。 依赖引用折叠:只要组合中出现左值引用,结果就是左值引用;只有右值引用与右值引用组合才仍是右值引用。
move 与 forward:保留值类别
命名变量即使类型是右值引用,表达式本身仍是左值。↡在转发引用函数中用 std::forward 和原始推断类型恢复调用点值类别,使左值继续作为左值、右值继续作为右值传给下一层。 使用 std::forward<T> 做条件转换:
template<typename F, typename T>
void relay(F f, T &&arg) {
f(std::forward<T>(arg));
}这里 std::move(arg) 会无条件把左值调用者也变成可移动对象,而 std::forward<T>(arg) 只在原实参是右值时恢复右值。转发函数要保留 const、引用和值类别,形参与 forward 的模板实参必须使用同一个推断 T。
重载与模板
↡普通函数与一个或多个函数模板共享名字,由普通重载决议、模板实参推断和函数模板偏序共同选择最佳候选。更特化不是按源码先后或主观复杂度判断。 先比较转换序列;候选同样好时,非模板函数通常优于模板实例,更特化的函数模板优于更泛化模板:
template<typename T> void inspect(T);
template<typename T> void inspect(T*);
void inspect(int*);
int *p = nullptr;
inspect(p); // 选择非模板 inspect(int*)函数模板特化不独立参加普通重载候选生成;先选主模板/重载,再考虑与该模板关联的显式特化。因此函数场景通常优先通过重载表达不同模式,避免特化与重载交互产生意外。
依赖名字、成员模板与友元
模板定义中依赖模板参数的限定名,编译器默认可能把它当值;若它表示类型,要写 typename T::value_type。调用依赖对象上的成员模板时,某些语境还需 object.template member<U>() 消除解析歧义。
类模板可以让所有实例互为友元,也可只授权匹配实例。成员模板本身不能是 virtual,因为虚函数表需要为类实例提供固定成员集合,而成员模板会按调用产生不同函数实例。
函数模板:从具体函数到类型无关的公式
你写 int max(int a, int b) { return a > b ? a : b; }——这个函数接受两个 int,返回较大的 int。要支持 double?再写 double max(double a, double b)——逻辑一模一样,只是类型名不同。
↡C++ 中用 template 关键字定义的、参数类型尚未确定的函数蓝图。编译器在调用点根据实参类型推导出模板参数,然后生成该类型的专属版本(实例化)。本质是编译器替你「复印」代码——你写一次泛型逻辑,编译器为你需要的每种类型生成一份。解决了这个问题——你把类型变成参数:
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}template<typename T> 声明了一个 ↡模板参数列表中用 typename 或 class 关键字声明的占位符——T、U 等名字代表「某种尚未确定的类型」。实例化时,编译器用实际类型(如 int、string)替换这个占位符。可以声明多个:typename T, typename U。 T。现在 max 不再绑定到某个具体类型——T 是一个占位符,编译器会在你来调用时自动推导它。
实例化:编译器如何"复印"出具体函数
当你写 int result = max(3, 7); 时,编译器做了四件事——这称为 ↡编译器根据模板和实际使用的类型参数,自动生成一份对应类型的代码的过程。分为隐式实例化(编译器从调用自动推导类型后生成)和显式实例化(程序员手动指定类型)。每个不同的类型实参组合都生成一份独立的代码——int 版和 double 版是两个完全独立的函数。:
隐式推导 vs 显式指定
大多数时候,编译器先从调用实参推断模板实参;当程序需要该专用版本时再发生 ↡程序使用模板专用版本时,由编译器按需生成声明或定义的过程。它与模板实参推断相关但不是同一个步骤,类模板成员通常只在被使用时实例化。。例如 max(3, 7) 先推断 T = int,随后按需实例化 int 版本。
有时推断不出来,或你要指定前几个模板参数,可在调用点写 ↡调用模板时由程序员直接提供一个或多个模板实参,未提供的其余参数仍可继续推断。它不是显式实例化定义。:
auto x = max<double>(3.2, 5); // 显式指定 T=double
// 5 会被转为 double另一个不同概念是 ↡程序员在指定翻译单元要求生成某个模板专用版本,或用 extern template 声明抑制本翻译单元重复实例化的机制。:
template int max<int>(int, int); // 显式实例化定义
extern template int max<long>(long, long); // 显式实例化声明显式实例化允许把常用专用版本集中到一个 .cpp 生成,其他翻译单元只保留 extern template 声明;这也是模板定义不必永远全部放头文件的一个受控例外。
模板参数推导的规则
编译器推导 T 时有几条重要规则:
- 每个模板参数必须能从函数参数推导出来——推导不出来就编译错误
- 同一个
T出现多次时,按相应形参模式推导的结果必须一致;推断阶段不会先做普通算术转换来统一int与double - 只有函数参数参与推导——返回类型不参与(如果返回类型是参数化的,需要显式指定或另加模板参数)
- 按值形参会忽略实参顶层 const,并发生数组/函数到指针调整;引用形参保留 const 和数组维度,转发引用还有专门的左值推断规则
类模板:让整个类变成类型无关的
你见过 vector<int>、vector<string>、vector<double>——它们都是同一个 ↡用 template 关键字定义的、类体中使用了类型参数的类。实例化时用具体类型替换类型参数——每个不同的类型实参组合都生成一个独立的类。标准容器(vector、list、map)全部是类模板。 vector 的不同实例化。这次把"整个类"的类型推迟到使用时再决定:
template<typename T>
class Stack {
public:
void push(const T &val);
T pop();
T top() const;
bool empty() const { return data.empty(); }
private:
std::vector<T> data;
};Stack<int> 内部是 vector<int>,Stack<string> 内部是 vector<string>——类的结构相同,但底层数据类型完全不同。Stack<int> 和 Stack<string> 是两个独立的类——它们之间没有任何继承或友元关系。
类外成员函数定义:必须重写 template 前缀
类的成员函数如果在类外定义——必须重写 template<typename T> 前缀,并用 ClassName<T>:: 限定作用域:
template<typename T>
void Stack<T>::push(const T &val) // Stack<T>:: 不能省略——Stack 不是类名
{
data.push_back(val);
}
template<typename T>
T Stack<T>::pop()
{
T topVal = data.back();
data.pop_back();
return topVal;
}两处容易踩坑:① 忘写 template<typename T>——编译器不认识类外的 T 是什么;② 误写 Stack::push 而不是 Stack<T>::push——Stack 只是一个模板名,不是真正的类名。只有 Stack<T>(带上类型参数)才是类。
模板的声明与定义:为什么要放头文件里
实例化点必须能获得模板定义,因此通用模板通常把定义放头文件。也可以在 .cpp 中保留定义并显式实例化一组封闭的专用版本,再由头文件提供 extern template 声明;代价是调用方不能任意使用未预先生成的新类型组合。
非类型模板参数:把值也变成参数
模板参数不止可以是类型,也可以是编译期值。↡以值而不是类型作为模板参数的形式。C++11 支持受限类别的整型、枚举、指针、引用等常量参数,常用于数组长度与策略选择;后续标准逐步放宽。:
template<typename T, std::size_t N>
class Array {
public:
T& operator[](std::size_t i) { return data[i]; }
constexpr std::size_t size() const { return N; }
private:
T data[N]; // N 是编译期常量——数组大小
};
Array<int, 10> arr1; // T=int, N=10
Array<double, 5> arr2; // T=double, N=5N 必须在编译时就能确定——它是常量表达式。不能用变量当非类型参数——int n = 5; Array<int, n> 编译错误。非类型模板参数常用的类型:整数、枚举、指针、引用(C++17 新增了 auto 类型的非类型参数 template<auto N>;C++20 进一步放宽支持浮点数和字面量类类型)。
标准库中的 std::array<T, N> 就是类模板 + 非类型模板参数的典范——array<int, 10> 是一个编译期固定大小的数组,不分配堆内存、性能媲美内置数组。
模板参数的默认值
模板参数也可以有默认值——和函数默认参数类似。标准库处处依赖这个机制:
template<typename T, typename Container = std::vector<T>>
class Stack {
Container data;
// ...
};
Stack<int> s1; // 使用默认:Stack<int, vector<int>>
Stack<int, std::deque<int>> s2; // 覆盖默认——底层用 deque模板特例化(模板特化):为特定类型定制实现
大部分时候,主模板适用于所有目标类型;特定参数组合或参数模式可能需要不同实现。标准库的 vector<bool> 就提供了专门语义。↡为已声明主模板的特定实参组合或类模板参数模式提供定制定义的机制。显式特化匹配完整组合,偏特化参与类模板模式偏序。 用于表达这种定制:
// 主模板——通用版本
template<typename T>
struct RemoveReference {
using type = T;
};
// 偏特化——匹配左值引用 T&
template<typename T>
struct RemoveReference<T&> {
using type = T;
};
// 偏特化——匹配右值引用 T&&
template<typename T>
struct RemoveReference<T&&> {
using type = T;
};三种形式在层次树中的关系:
全特化 vs 偏特化
- ↡为主模板的一个完整模板实参组合提供独立定义的显式特化。函数模板和类模板都可显式特化,但函数模板特化会受重载选择顺序影响。:所有参数都固定,如
template<> struct X<int>。 - ↡为类模板的某类参数模式提供仍带模板参数的定义。多个可行偏特化通过偏序选择更特化者;函数模板不支持偏特化,应使用重载。:如
template<typename T> struct X<T*>匹配指针模式。
调用 RemoveReference<int&>::type 时,主模板与 T& 偏特化都可匹配,类模板偏序判定 T& 模式更特化,因此选择它并得到 int。若多个偏特化彼此都不更特化,程序会因二义性失败;不能用一条脱离模式关系的固定优先级口诀代替偏序规则。
函数模板只能全特化
// 主模板
template<typename T>
int compare(const T &a, const T &b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
// 全特化——为 const char* 定制
template<>
int compare(const char* const &a, const char* const &b) {
return std::strcmp(a, b); // 对字符串——比字典序而不是比指针地址
}函数模板不能偏特化——如果需要针对某种模式(如 T*)做不同处理,用重载一个新的模板函数而不是写特化。重载优先,特化容易有隐蔽的匹配意外。
可变参数模板:接受任意数量、任意类型的参数
前面的模板参数数量是固定的——一个 T、两个 T, U。如果需要接受任意数量的参数(像 printf 那样),就需要 ↡C++11 引入的、接受零个或多个模板参数的模板。用 typename... 或 class... 声明参数包(parameter pack),通过递归展开(每次取出一个参数,对剩余包递归调用自身)逐个处理。sizeof...(Args) 查询包中参数数量。递归终止靠空包匹配到无参版本(base case)。:
// Base case——终止递归
void print() { }
// 可变参数模板——每次取出第一个,对剩下的递归
template<typename T, typename... Args>
void print(const T &first, const Args&... rest) {
std::cout << first;
if (sizeof...(rest) > 0) { // C++11:条件是编译期常量
std::cout << ", ";
}
print(rest...); // 递归调用,rest... 被展开
}
print("hello", 42, 3.14, 'x'); // → "hello, 42, 3.14, x"typename... Args 声明了 ↡代表零个或多个模板参数或函数参数的集合。sizeof... 查询元素数量,包本身必须放在包含省略号的模式中展开。。↡把包含参数包名字的模式按包中每个元素重复,生成类型、形参、实参或表达式序列。省略号作用于整个模式而不只是紧邻名字。 rest... 会把全部剩余实参传给下一次调用;递归只是消费展开结果的一种方式。
下面就通过 Stepper 分步看 print("hello", 42, 3.14) 这个调用如何一步步递归展开,直到参数包为空、调用终止。
动手:一步步看模板如何从蓝图变成代码、参数包如何被递归吃掉
下面用两个 Stepper 演示模板编程中最核心的两条"动起来"的机制:① 函数模板从声明到编译器生成实例的全过程;② 可变参数模板每次递归吃掉一个参数直到空包终止。每条步骤都配图示把抽象规则翻译成可视化的步骤。
函数模板实例化四阶段
猜一猜:写
template<typename T> T max(T a, T b)后,什么时候才会生成int max(int, int)?第一次写这行代码的时候?还是第一次用它的时候?用 Stepper 验证你的猜想。
① 模板声明——只写蓝图,不生成代码
template<typename T> 只是向编译器注册了一个蓝图。此时 T 还是占位符——没有类型确定、没有任何代码生成。就像图纸放在抽屉里——不占成品库空间。
可变参数模板递归展开
猜一猜:
print("a", 1, 2.5)这个调用——最终会产生几次函数调用?如果忘了写终止条件(base case),编译器会怎样?用 Stepper 逐步验证。
① 初始调用——参数包包含三个参数
print("hello", 42, 3.14) 中,T 按 const 引用形参保留字符串字面量的数组类型信息,Args = {int, double},所以 sizeof...(Args) == 2。随后展开 rest 调用 print(42, 3.14)。
代码逐段拆解:完整模板示例
以下是覆盖本章五个核心知识点的完整实现——函数模板、类模板、非类型参数、特化、可变参数模板——每段有独立的设计决策解说。
函数模板:通用的比较与打印
#include <iostream>
#include <cstring>
// 通用比较——只要类型支持 < 就能用
template<typename T>
int compare(const T &a, const T &b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}const T& 避免大对象拷贝。T 只需要支持 < 运算——这就是模板的"隐式接口":模板不要求继承某个基类、不要求显式声明接口——只要实际类型能用 < 就行。如果 T 没有定义 <,编译期就能发现(而不是运行期崩溃)——这叫编译期多态。
全特化:为 const char* 定制
// 全特化——const char* 不能用 < 比字典序
template<>
int compare(const char* const &a, const char* const &b) {
return std::strcmp(a, b); // strcmp 返回 <0 / 0 / >0
}当 compare(a, b)(a、b 是 const char* 指针变量)被调用时——如果走通用版本,比较的是两个指针地址而不是字符串内容。全特化版本改用 strcmp 比较字典序——这是覆盖默认逻辑的正确方式。注意参数类型中的 const char* const&——引用指向的指针本身是 const(不能修改指针),指针指向的是 const char。
注意一个著名陷阱:直接传字符串字面量 compare("abc", "xyz") 不会命中这个全特化。因为字面量 "abc" 的类型是数组 const char[4] 而非指针,T 会被推导为数组类型、走通用模板(用 < 比较数组)。要命中 const char* 全特化,得先把字面量转成指针变量:const char* a = "abc"; const char* b = "xyz"; compare(a, b);。
类模板 Stack:完整的类外定义
template<typename T>
class Stack {
public:
void push(const T &val);
T pop();
T top() const;
bool empty() const { return data.empty(); }
std::size_t size() const { return data.size(); }
private:
std::vector<T> data;
};
// 类外定义——每个都需要 template 前缀
template<typename T>
void Stack<T>::push(const T &val) { data.push_back(val); }
template<typename T>
T Stack<T>::pop() {
if (data.empty()) throw std::out_of_range("pop empty stack");
T val = data.back();
data.pop_back();
return val;
}
template<typename T>
T Stack<T>::top() const {
if (data.empty()) throw std::out_of_range("top empty stack");
return data.back();
}类内 empty 和 size 直接在类体内定义——不需要类外的 template 前缀。类外的 push、pop、top 必须各自带上 template<typename T> 和 Stack<T>:: 作用域。pop 和 top 加了空栈检查——抛出异常而不是未定义行为。pop 返回值(不是引用)——因为 pop 后栈顶元素被销毁了,返回的必须是转移出去的副本。
非类型模板参数:编译期确定大小的数组
template<typename T, std::size_t N>
class FixedArray {
public:
T& operator[](std::size_t i) { return data[i]; }
const T& operator[](std::size_t i) const { return data[i]; }
constexpr std::size_t size() const { return N; }
T* begin() { return data; }
T* end() { return data + N; }
const T* begin() const { return data; }
const T* end() const { return data + N; }
private:
T data[N];
};N 是编译期常量——size() 可以标记为 constexpr(编译期可算)。支持 begin()/end() 意味着可以和范围 for 循环配合——for (auto &x : arr)。注意下标的 const 重载——和前面章节学的 operator[]一样,const 版返回 const T& 保证只读。
可变参数模板:递归打印
// Base case——参数包为空时终止
template<typename T>
std::ostream& print(std::ostream &os, const T &t) {
return os << t;
}
// Variadic——每次取出第一个,对剩下的递归
template<typename T, typename... Args>
std::ostream& print(std::ostream &os, const T &t, const Args&... rest) {
os << t << ", ";
return print(os, rest...); // 递归
}这里用单参数重载作为终止条件:可变参数版本只处理“后面仍有参数”的调用并输出逗号,最后一个参数由更具体的单参数重载输出。整段代码符合 C++11,不依赖 C++17 的 if constexpr。
容易踩的坑
小结
- 模板是"代码模具"——函数模板和类模板让你把类型变成参数,一套逻辑对所有类型通用。编译器在调用点自动推导类型、生成实例——每个不同的类型组合 = 一份独立的代码
- 类外定义成员函数时,必须重写
template<typename T>前缀 +ClassName<T>::作用域限定。Stack只是模板名,Stack<T>才是类名——这点写错就编译失败 - 非类型模板参数把值也变成编译期参数——
template<int N>让数组大小由模板决定。std::array<T, N>是这个模式的标准用法 - 特化是为特定类型定制实现——全特化(所有参数固定)、偏特化(限定模式如
T*)。函数模板不能偏特化——用重载一个更具体的模板来替代 - 可变参数模板(
typename... Args)接受任意数量参数——通过递归每次消耗一个参数,直到空包触发终止。sizeof...(Args)在编译期告诉你包里还剩多少
练习
问题 1(改代码型) 下面的 Stack 类模板有三个问题——找出它们并写出修正后的完整版本。
template<typename T>
class Stack {
public:
void push(const T &val) { data.push_back(val); }
T pop() { T v = data.back(); data.pop_back(); return v; }
// 缺少 top()
// 缺少 empty()
private:
std::vector<T> data;
};
// 问题版本:类外定义的 print 函数
template<typename T>
void Stack::print() const {
for (size_t i = data.size(); i > 0; --i)
std::cout << data[i-1] << " ";
std::cout << "\n";
}问题 2(独立实现题) 设计并实现一个 Pair<T, U> 类模板:
- 有两个公有成员:
T first; U second; - 提供两个类外定义的成员函数:
Pair make_pair(T f, U s)(静态工厂)和void swap(Pair &other) - 提供一个非成员函数模板
operator==(比较两个Pair——first和second都相等才为 true) - 写出使用示例:创建
Pair<int, string>、Pair<double, char>并测试比较运算符
问题 3(问答型) Stack<int> 和 Stack<string> 之间是什么关系?如果 Stack<int> 的一个成员函数用到某个 static 局部变量——Stack<string> 会共享同一个 static 变量吗?为什么?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 函数模板(function template)
C++ 中用
template关键字定义的、参数类型尚未确定的函数蓝图。模板本身不是函数——只是"代码模具"。当你调用时,编译器根据实参类型推导模板参数、生成该类型的专属版本(实例化)。一次编写、多类型通用——是消除"只有类型不同"的重复代码的主要手段。本质是编译器替你"复印"。- 类型参数(type parameter)
模板参数列表中用
typename或class声明的占位符——T、U等名字代表"某种尚未确定的类型"。你可以把它理解成一个变量——只是这个变量的值不是数字,而是一个类型(如int、string)。实例化时,编译器用实际类型替换这个占位符。在同一个模板中,类型参数可以出现在类型名、参数类型、返回类型等任何需要写类型的位置。- 模板实例化(template instantiation)
编译器根据模板和实际使用的类型参数,自动生成一份对应类型代码的过程。分隐式(编译器从调用自动推导类型后生成)和显式(程序员手动写出具体类型)。每个不同的类型参数组合都生成一份独立的代码——
max<int>和max<double>是两个完全独立的函数。实例化发生在编译期——没有运行期开销。- 隐式实例化(implicit instantiation)
程序需要模板专用版本时,编译器按需生成其声明或定义的过程。模板实参推断通常先决定参数,但推断与实例化是不同阶段;类模板未使用的成员通常不会被实例化。
- 显式模板实参(explicit template arguments)
调用模板时直接写出一个或多个模板实参;未写出的其余参数仍可推断。它用于无法从函数形参推断的参数或主动指定类型,不等同于显式实例化定义。
- 显式实例化(explicit instantiation)
在指定翻译单元要求生成某个模板专用版本的定义,或用 extern template 声明抑制本翻译单元重复实例化。它可把封闭类型集合的模板代码集中到 cpp 文件。
- 类模板(class template)
用
template关键字定义的、类体中使用了类型参数的类。它不是真正的类——是类的蓝图。实例化后每个不同的类型参数组合都生成一个独立的类:vector<int>和vector<string>是两个完全不同的类型、彼此无任何关系。所有标准容器(vector、list、map、array等)都是类模板。- 非类型模板参数(non-type template parameter)
模板参数列表中不是类型名、而是一个具体的值——如
template<int N>中的N。编译期就必须知道它的值(常量表达式)。常用于指定数组大小、容器容量等编译期确定的值。常用的非类型参数类型包括整数、枚举、指针、引用。std::array<T, N>是最标准的用法——N是元素个数。- 模板特化(template specialization)
为主模板的特定完整实参组合或类模板参数模式提供定制定义。显式特化匹配完整组合,偏特化参与模式偏序;函数模板特化还与普通重载选择顺序交互。
- 全特化(full specialization)
为主模板的完整实参组合提供独立定义的显式特化。函数模板和类模板都可显式特化;函数模板特化不会像普通重载那样独立参加候选生成。
- 偏特化(partial specialization)
为类模板的一类参数模式提供仍带模板参数的定义。多个偏特化通过偏序选择更特化者;彼此都不更特化会二义。函数模板不支持偏特化,应使用重载。
- 可变参数模板(variadic template)
C++11 引入的、接受零个或多个模板参数的模板。参数包可通过模式扩展生成类型或表达式序列,递归重载只是 C++11 中消费包的一种常见方式。
- 参数包(parameter pack)
代表零个或多个模板参数或函数参数的集合。sizeof... 返回元素数,包名字必须出现在带省略号的模式中才能展开。
- 包扩展(pack expansion)
把包含参数包的模式按包中每个元素重复,生成类型、形参、实参或表达式序列。省略号作用于完整模式,递归不是唯一展开方式。
- 模板实参推断(template argument deduction)
根据函数实参与形参模式推导模板参数。推断发生在普通隐式转换之前,按值、引用、数组与 const 会采用不同调整规则。
- 转发引用(forwarding reference)
需要推断的 T 对应右值引用形参。传左值时 T 推断为左值引用,传右值时 T 为非引用类型,并通过引用折叠形成最终形参类型。
- 完美转发(perfect forwarding)
在转发引用函数中使用 std::forward 与原始推断 T,恢复调用点的左值或右值类别并传给下一层。
- 模板重载(template overloading)
普通函数和函数模板共享名字,由转换序列、模板推断和函数模板偏序选择最佳候选。同等转换时非模板通常优先,更特化模板优于泛化模板。
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
重载与模板:机制、边界与证据
在《模板与泛型编程》的官方单元 cppp-16 中,重载与模板连接本章第 3 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。
模板特例化:机制、边界与证据
在《模板与泛型编程》的官方单元 cppp-16 中,模板特例化连接本章第 5 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。