模板与泛型编程

掌握C++模板编程——函数模板/类模板/模板实例化/模板参数推导/可变参数模板——写出类型无关的泛型代码(类设计者工具篇收官)

学习目标

  • 能解释函数模板和类模板的工作机制——编译器如何在编译期从模板生成具体类型的代码,隐式和显式实例化的区别,以及类型参数推导的规则
  • 能自己写出泛型的数据结构(Stack<T>Pair<T,U>)和泛型算法(findcompare),正确处理类外成员函数定义的 template 前缀和 ClassName<T>:: 作用域限定
  • 能回答:写一个 Stack 类模板,支持 pushpoptopempty——写出头文件中的完整模板声明(类体 + 类外成员函数定义),并说明 Stack<int>Stack<string> 这两个"类"之间是什么关系

机制总览

模板与泛型编程:机制路径

  1. 1

    直觉:为什么需要模板

    你写了一个排序函数——接受 int 数组排序。老板说"同样的排序逻辑, double 数组也要"。你复制粘贴,改了类型。老板又说 string 数组也要、 long 也要——你复制了四次几乎一模一样的代码,只改了类型名。每次代码逻辑出现 bug,你要改四个地方。这显然不是一个好办法。

  2. 2

    官方 Chapter 16 的完整模板解析链

    模板调用不是简单的文本替换。编译器先形成候选,推断模板实参,进行重载决议与模板偏序,再把选中模式代入并检查实例语义。某个类型能写进模板参数,不代表模板体里的全部操作对它都有效。

  3. 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 替换成实际类型——intdoublestring——帮你批量生成不同类型的版本。

这一章解决什么问题? 教你用模板写出"类型无关"的代码——一套逻辑,所有类型通用。你不再需要为 int 写一遍、为 double 写一遍、为 string 写一遍——写一次就够了。没有模板,你的代码库会被大量"只有类型不同"的重复代码撑爆——更可怕的是每份副本都可能藏着彼此不同步的 bug。模板是 C++ 标准库的基石——从 vector<int>sort(v.begin(), v.end())——它们全都是用模板实现的。

官方 Chapter 16 的完整模板解析链

模板调用不是简单的文本替换。编译器先形成候选,推断模板实参,进行重载决议与模板偏序,再把选中模式代入并检查实例语义。某个类型能写进模板参数,不代表模板体里的全部操作对它都有效。

模板实参推断、引用折叠与数组

不会为了让多个实参“看起来兼容”而先做普通算术转换,所以 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");

依赖引用折叠:只要组合中出现左值引用,结果就是左值引用;只有右值引用与右值引用组合才仍是右值引用。

move 与 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)——逻辑一模一样,只是类型名不同

解决了这个问题——你把类型变成参数:

template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

template<typename T> 声明了一个 T。现在 max 不再绑定到某个具体类型——T 是一个占位符,编译器会在你来调用时自动推导它。

实例化:编译器如何"复印"出具体函数

当你写 int result = max(3, 7); 时,编译器做了四件事——这称为

模板实例化的四阶段过程① 模板声明用户写 template<typename T>T max(T a, T b) { …… }② 函数调用编译器遇到 max(3, 7);——模板还未生成代码③ 隐式推导编译器推导 T = int检查签名是否匹配④ 生成实例生成 int max(int,int)插入对它的调用当前阶段:模板声明——只写了蓝图,还没生成任何代码template<typename T>T max(T a, T b) { return a > b ? a : b; }
阶段一:模板只是一个蓝图——只有 template 声明,没有任何类型确定、没有任何代码生成。

隐式推导 vs 显式指定

大多数时候,编译器先从调用实参推断模板实参;当程序需要该专用版本时再发生 。例如 max(3, 7) 先推断 T = int,随后按需实例化 int 版本。

有时推断不出来,或你要指定前几个模板参数,可在调用点写

auto x = max<double>(3.2, 5);    // 显式指定 T=double
                                   // 5 会被转为 double

另一个不同概念是

template int max<int>(int, int);        // 显式实例化定义
extern template int max<long>(long, long); // 显式实例化声明

显式实例化允许把常用专用版本集中到一个 .cpp 生成,其他翻译单元只保留 extern template 声明;这也是模板定义不必永远全部放头文件的一个受控例外。

模板参数推导的规则

编译器推导 T 时有几条重要规则:

  • 每个模板参数必须能从函数参数推导出来——推导不出来就编译错误
  • 同一个 T 出现多次时,按相应形参模式推导的结果必须一致;推断阶段不会先做普通算术转换来统一 intdouble
  • 只有函数参数参与推导——返回类型不参与(如果返回类型是参数化的,需要显式指定或另加模板参数)
  • 按值形参会忽略实参顶层 const,并发生数组/函数到指针调整;引用形参保留 const 和数组维度,转发引用还有专门的左值推断规则

类模板:让整个类变成类型无关的

你见过 vector<int>vector<string>vector<double>——它们都是同一个 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 声明;代价是调用方不能任意使用未预先生成的新类型组合。

非类型模板参数:把值也变成参数

模板参数不止可以是类型,也可以是编译期值。

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=5

N 必须在编译时就能确定——它是常量表达式。不能用变量当非类型参数——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;
};

三种形式在层次树中的关系:

模板特化层次树(从最通用到最具体)主模板(Primary Template)template<typename T> struct Remove{...};更具体偏特化 T*template<typename T>struct Remove<T*>{...};偏特化 T&template<typename T>struct Remove<T&>{...};偏特化 T&&template<typename T>struct Remove<T&&>{...};全特化 inttemplate<> struct Remove<int>{...};匹配优先级:全特化 > 偏特化 > 主模板编译器总是选择"最具体"的版本——参数全部固定 > 部分固定 > 全部开放。找不到特化才用主模板。
主模板是最通用的蓝图。偏特化限定了部分参数模式(如指针版 T*)。全特化固定了所有参数(如 int 版)。编译器总是选择 最具体的版本——全特化 > 偏特化 > 主模板。找不到特化才用主模板。

全特化 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 那样),就需要

// 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 声明了 rest... 会把全部剩余实参传给下一次调用;递归只是消费展开结果的一种方式。

下面就通过 Stepper 分步看 print("hello", 42, 3.14) 这个调用如何一步步递归展开,直到参数包为空、调用终止。

动手:一步步看模板如何从蓝图变成代码、参数包如何被递归吃掉

下面用两个 Stepper 演示模板编程中最核心的两条"动起来"的机制:① 函数模板从声明到编译器生成实例的全过程;② 可变参数模板每次递归吃掉一个参数直到空包终止。每条步骤都配图示把抽象规则翻译成可视化的步骤。

函数模板实例化四阶段

猜一猜:写 template<typename T> T max(T a, T b) 后,什么时候才会生成 int max(int, int)?第一次写这行代码的时候?还是第一次用它的时候?用 Stepper 验证你的猜想。

分步1 / 4

① 模板声明——只写蓝图,不生成代码

模板实例化的四阶段过程① 模板声明用户写 template<typename T>T max(T a, T b) { …… }② 函数调用编译器遇到 max(3, 7);——模板还未生成代码③ 隐式推导编译器推导 T = int检查签名是否匹配④ 生成实例生成 int max(int,int)插入对它的调用当前阶段:模板声明——只写了蓝图,还没生成任何代码template<typename T>T max(T a, T b) { return a > b ? a : b; }
阶段一:模板只是一个蓝图——只有 template 声明,没有任何类型确定、没有任何代码生成。

template<typename T> 只是向编译器注册了一个蓝图。此时 T 还是占位符——没有类型确定、没有任何代码生成。就像图纸放在抽屉里——不占成品库空间

可变参数模板递归展开

猜一猜:print("a", 1, 2.5) 这个调用——最终会产生几次函数调用?如果忘了写终止条件(base case),编译器会怎样?用 Stepper 逐步验证。

分步1 / 5

① 初始调用——参数包包含三个参数

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)abconst 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();
}

类内 emptysize 直接在类体内定义——不需要类外的 template 前缀。类外的 pushpoptop 必须各自带上 template<typename T>Stack<T>:: 作用域。poptop 加了空栈检查——抛出异常而不是未定义行为。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——firstsecond 都相等才为 true)
  • 写出使用示例:创建 Pair<int, string>Pair<double, char> 并测试比较运算符

问题 3(问答型) Stack<int>Stack<string> 之间是什么关系?如果 Stack<int> 的一个成员函数用到某个 static 局部变量——Stack<string> 会共享同一个 static 变量吗?为什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

函数模板(function template)

C++ 中用 template 关键字定义的、参数类型尚未确定的函数蓝图。模板本身不是函数——只是"代码模具"。当你调用时,编译器根据实参类型推导模板参数、生成该类型的专属版本(实例化)。一次编写、多类型通用——是消除"只有类型不同"的重复代码的主要手段。本质是编译器替你"复印"。

类型参数(type parameter)

模板参数列表中用 typenameclass 声明的占位符——TU 等名字代表"某种尚未确定的类型"。你可以把它理解成一个变量——只是这个变量的值不是数字,而是一个类型(如 intstring)。实例化时,编译器用实际类型替换这个占位符。在同一个模板中,类型参数可以出现在类型名、参数类型、返回类型等任何需要写类型的位置。

模板实例化(template instantiation)

编译器根据模板和实际使用的类型参数,自动生成一份对应类型代码的过程。分隐式(编译器从调用自动推导类型后生成)和显式(程序员手动写出具体类型)。每个不同的类型参数组合都生成一份独立的代码——max<int>max<double> 是两个完全独立的函数。实例化发生在编译期——没有运行期开销。

隐式实例化(implicit instantiation)

程序需要模板专用版本时,编译器按需生成其声明或定义的过程。模板实参推断通常先决定参数,但推断与实例化是不同阶段;类模板未使用的成员通常不会被实例化。

显式模板实参(explicit template arguments)

调用模板时直接写出一个或多个模板实参;未写出的其余参数仍可推断。它用于无法从函数形参推断的参数或主动指定类型,不等同于显式实例化定义。

显式实例化(explicit instantiation)

在指定翻译单元要求生成某个模板专用版本的定义,或用 extern template 声明抑制本翻译单元重复实例化。它可把封闭类型集合的模板代码集中到 cpp 文件。

类模板(class template)

template 关键字定义的、类体中使用了类型参数的类。它不是真正的类——是类的蓝图。实例化后每个不同的类型参数组合都生成一个独立的类:vector<int>vector<string> 是两个完全不同的类型、彼此无任何关系。所有标准容器(vectorlistmaparray 等)都是类模板。

非类型模板参数(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 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。

讨论

评论区加载中…