template
多态的一种形式
参数化模块
- 对程序模块加上类型参数
- 对不同类型的数据实施相同的操作
模板参数的规则
这一页的重点在于模板参数的灵活性,分为两种情况:
A. 多类型参数 (Multiple Type Parameters)
-
规则: 模板可以接受多个不同的类型参数,中间用逗号分隔。
-
语法示例:
template <class T1, class T2> void f(T1 a, T2 b) { // T1 和 T2 可以是不同类型,比如一个 int 一个 double ... }
B. 混合普通参数 (Non-Type Parameters / Ordinary Parameters) 这是比较特殊的高级用法,模板里不仅可以传“类型(Type)”,还可以传“数值(Value)”。
-
规则 1(位置): 普通参数(如
int size)必须列在所有类型参数(如class T)之后。 -
规则 2(调用): 调用时,因为编译器很难根据函数参数推断出这个数值,所以通常需要显式指定模板实参。
-
语法示例:
// 定义:T 是类型,size 是一个整数常量 template <class T, int size> void f(T a) { T temp[size]; // 利用传入的 size 在栈上开数组 ... } // 调用:必须显式告诉编译器 T是int,size是10 f<int, 10>(1);
万能引用与完美转发
转发失效
template <typename T>
void transfer(T&& arg) { // arg 是万能引用
std::cout << "收到参数..." << std::endl;
process(arg); // <--- 问题出在这里:arg 有名字,所以它是左值!
}
// 调用端
transfer(std::string("world")); // 传入的是临时对象(右值)
【关键点分析】
- 场景:我们希望编写一个中间函数
transfer,将参数原封不动地传给process。 - 期望:传入右值(临时对象),
process应该调用右值版本void process(string&&)以获得最高效率。 - 现实:实际调用了左值版本
void process(const string&)。 - 原因:在 C++ 中,只要变量有名字(如
arg),它就是左值,无论它的类型是不是右值引用。
使用 std::forward 还原参数本色
template <typename T>
void perfectTransfer(T&& arg) {
// std::forward<T> 根据 T 的类型进行“条件转换”
process(std::forward<T>(arg));
}
【原理解析】
- 工具:
std::forward<T>()。 - 作用:它像一个“时光机”,查看参数
arg在进入函数之前是什么状态。- 如果原始参数是左值 $\rightarrow$ 它就以左值形式转发。
- 如果原始参数是右值 $\rightarrow$ 它就强制转回右值形式转发(相当于
move)。
函数特化
#include <iostream>
#include <cstring>
// 1. 通用模板
template <typename T>
bool areEqual(T a, T b) {
std::cout << "调用了通用模板" << std::endl;
return a == b;
}
// 2. 针对 const char* 的显示具体化(全特化)
template <>
bool areEqual<const char*>(const char* a, const char* b) {
std::cout << "调用了 const char* 特化版本" << std::endl;
// 使用 strcmp 比较内容,而不是地址
return std::strcmp(a, b) == 0;
}
C++ 编译器在决定调用哪个函数时(重载决议),流程是这样的:
- 先看所有的普通函数:如果有完全匹配的普通函数(如方案 B),直接选中,结束。
- 再看基础模版:如果没有普通函数匹配,才去查基础模版。
- 最后看特化:只有确定要用基础模版了,才会去检查“这个模版有没有针对该类型的特化”。
是否实例化模板的某个实例由使用点来决定;如果未使用到一个模板的某个实例,则编译系统不会生成相应实例的代码
模版的完整定义(声明 + 实现)通常必须都写在头文件 (.h) 中
否则会链接错误
错误的代码结构示例:
1. 头文件 (file1.h) - 只有声明
// file1.h
#pragma once
// 类模板声明
template <class T>
class S {
T a;
public:
void f(); // 只有函数声明
};
// 函数模板声明
template <class T>
T max(T x, T y);
2. 实现文件 (file1.cpp) - 包含定义
// file1.cpp
#include "file1.h"
// 类模板成员函数的外部定义
template <class T>
void S<T>::f() {
// ... 具体实现 ...
}
// 函数模板的定义
template <class T>
T max(T x, T y) {
return x > y ? x : y;
}
// main 函数中只用了 int 版本
void main_in_file1() {
int a = 1, b = 2;
max(a, b); // 编译器生成 max<int> 的代码
S<int> x;
x.f(); // 编译器生成 S<int>::f 的代码
}
// 注意:此时编译器并没有生成 max<double> 或 S<float> 的代码!
3. 调用文件 (file2.cpp) - 链接报错发生地
// file2.cpp
#include "file1.h"
// 或者使用 extern 声明(如 PPT 所示)
// extern double max(double, double);
void sub() {
// 【Error 1】链接错误
// 链接器试图寻找 max<double> 的二进制代码,但在 file1.obj 中找不到。
max(1.1, 2.2);
// 【Error 2】链接错误
// 链接器试图寻找 S<float>::f 的代码,同样找不到。
S<float> x;
x.f();
}
if constexpr —— 编译期分支优化
1.问题引入
场景: 我们想写一个“万能转字符串”函数 autoToString。
- 如果传入的已经是
string,直接返回。 - 如果传入的是数字(int, float等),调用
std::to_string转换。
错误写法(使用普通 if):
template <typename T>
std::string autoToString(T val) {
// std::is_same_v 判断 T 是否为 string
if (std::is_same_v<T, std::string>) {
return val;
} else {
// 问题出在这里!
return std::to_string(val);
}
}
为什么报错?(图中标红的问号)
虽然逻辑上我们知道,当 T 是 string 时会走 if 分支,但是,普通的 if 是运行时判断的。
- 编译器在编译这段模板时,必须确保两个分支的代码都是合法的。
- 当
T是std::string时,编译器会检查else分支:return std::to_string(val);。 - 报错原因:
std::to_string不接受std::string类型的参数。虽然代码永远跑不到这里,但编译器“看到了”它,并报了语法错。
- 解决方案 (Slide 2)
引入 if constexpr (C++17)
if constexpr 告诉编译器:“请在编译阶段就计算这个条件。如果条件为真,丢弃 else 分支;如果为假,丢弃 if 分支。”
正确写法:
template <typename T>
std::string autoToString(T val) {
// 加上 constexpr 关键字
if constexpr (std::is_same_v<T, std::string>) {
return val;
} else {
return std::to_string(val);
}
}
- 编译器的“剪枝”行为 (核心原理)
-
当 T = string 时:
条件为真。编译器直接删除 else 代码块。
实际生成的代码变成了:
std::string autoToString(std::string val) { return val; // else 里的 std::to_string(val) 根本不存在于最终的二进制代码中,所以不会报错 } -
当 T = int 时:
条件为假。编译器直接删除 if 代码块。
实际生成的代码变成了:
std::string autoToString(int val) { return std::to_string(val); }
| 特性 | 普通 if | if constexpr |
|---|---|---|
| 生效阶段 | 运行时 (Runtime) | 编译时 (Compile-time) |
| 分支编译 | 所有分支都必须语法正确,必须能被编译 | 只有条件满足的分支会被编译,被丢弃的分支甚至可以是语法上对当前类型无效的代码 |
用constexpr来优化代码 原本需要函数特化的部分可以用constexpr来优化

auto 在推导类型时,性格比较“霸道”,它通常会剥离掉原变量的“外衣”(引用和 const),只把最核心的数据类型拿过来做一份拷贝。
- “默认情况下是值拷贝”
-
含义:当你用
auto去接一个变量时,默认是复制(Copy)一份新的数据,而不是引用原数据。 -
后果:修改新的变量,不会影响原来的变量。
-
对应图中代码:
// vec 里面是 {1, 2, 3...} for (auto v : vec) { v = v * 2; // 无效修改! }这里的
v是vec中元素的副本。你把副本乘以 2 了,但在vec里的原版数据纹丝不动。
- “丢弃引用和 const 限定符”
这是 auto 最容易让人踩坑的地方。它会把顶层 const(Top-level const)和引用(Reference)全部扔掉。
-
丢弃引用:
int x = 10; int& ref = x; // ref 是 x 的引用 auto a = ref; // 【注意】a 推导出来是 int,而不是 int& a = 20; // x 还是 10,因为 a 只是个拷贝 -
丢弃顶层 const(即变量本身不可变):
const int b = 10; // b 是不可修改的 auto c = b; // 【注意】c 推导出来是 int,丢掉了 const c = 20; // c 可以被修改!因为 c 只是拷贝了一份 b 的值,c 自己没说不能改
- “不会忽略底层 const”
这是唯一的例外!这句话里的括号解释很关键:(指针指向的内容)。
-
含义:如果原来的变量指向了一个“只读区域”(比如字符串字面量),
auto就算再霸道,也不敢把这个“只读”属性丢掉,否则程序就会因为试图修改只读内存而崩溃。 -
对应图中代码:
// "Hello" 存储在常量区,不可修改 const char* s = "Hello"; auto s_auto = s; // s_auto 推导结果依然是 const char* // 如果把 const 丢了变成 char*,你就能修改 "Hello" 了,这在 C++ 里是违法的。
| 原类型 | 写法 | auto 推导结果 | 说明 |
|---|---|---|---|
int& (引用) |
auto x = ... |
int |
引用被丢弃,变成值拷贝 |
const int (常量) |
auto x = ... |
int |
const 被丢弃,变成可读写变量 |
const char* (底层const) |
auto x = ... |
const char* |
const 被保留,指向的内容依然只读 |