2. 模板
- 模板:
- 一种源代码复用机制
- 参数化模块
- 对程序模块加上类型参数
- 对不同类型的数据实施相同的操作
- 模板是多态的一种形式
- 解决同一操作作用于不同类型数据的问题
不方便的方式:
-
类属函数的宏实现
cpp#define max(a,b) ((a) > (b) ? (a) : (b))- 缺陷:
- 无类型检查(宏定义是在预处理阶段展开的, 编译器无法进行类型检查)
- 只能实现简单的功能
- 缺陷:
-
函数重载
- 缺陷:
- 需要为每种类型都定义一个函数
- 代码重复, 难以维护, 而且容易定义不全
- 缺陷:
-
函数指针
void sort(coid *, unsigned int, unsigned int, int (*cmp)(const void*, const void*));- 缺陷:
- 需要进行强制类型转换, 不安全
- 代码复杂, 可读性差
综上, template 引入的目标 : 完全, 清晰的模板
2.1 函数模板
cpp
template <typename T>
void sort(T A[], unsigned int n){
//排序算法
for (unsigned int i=0; i<n-1; i++)
for (unsigned int j=i+1; j<n; j++)
if (A[i] > A[j]) {
T temp = A[i];
A[i] = A[j];
A[j] = temp;
}
}
int main(){
int a[] = {3,5,1,4,2};
sort<int>(a,5);//显示指定类型参数
double b[] = {3.1,5.2,1.3,4.4,2.5};
sort(b,5);//隐式指定类型参数
}
对于内置类型可以使用, 对于自定义操作类型也可以:
- 必须重载操作符 >
- 必须有拷贝构造
- 必须有赋值操作符
模板:
-
函数模板定义了一类重载的函数
-
编译系统自动实例化函数模板
-
可以有多个类型参数, 用逗号分隔
cpptemplate <typename T1, typename T2> void func(T1 a, T2 b){ ... }
-
-
函数模板的参数:
- 可带普通参数:必须列在类型参数之后, 调用时需显式模板实参指定(编译期常量 )
template <typename T, int size> void func(T a){ T temp[size]; ... }
f<int, 10>(a);
- 可带普通参数:必须列在类型参数之后, 调用时需显式模板实参指定(编译期常量 )
万能引用和完美转发
下面代码存在问题: 传入的参数 arg 无论是左值还是右值, 在传递给 process 函数时都会被视为左值, 导致无法调用处理右值的重载版本.
cpp
void process(const std::string& s); // 处理左值
void process(std::string&& s); // 处理右值
template <typename T>
void transfer(T&& arg) {
std::cout << "收到参数" << std::endl;
process(arg); // 这里的 arg 是右值引用, 实际上是左值
}
为了解决这个问题, 我们可以使用 std::forward 来实现完美转发, 保持参数的值类别(左值或右值)不变.
- 如果 T 推导出来是左值, 那么就转发为左值
- 如果 T 推导出来是右值, 那么就转发为右值
- 这里的 T&& 被称为万能引用 (Universal Reference)或转发引用(Forwarding Reference)
cpp
template <typename T>
void perfectTransfer(T&& arg) {
std::cout << "收到参数" << std::endl;
process(std::forward<T>(arg)); // 完美转发
}
int main() {
std::string str = "Hello";
perfectTransfer(str); // 传入左值
perfectTransfer(std::string("Hi")); // 传入右值
// 消解引用的方式: 引用折叠
}
显式具体化(全特化)
- 首先是通用模板形式, 后面我们给出显式具体化
cpp
template <typename T>
bool isEqual(T a, T b){
return a == b;
}
int main() {
// Case1: 整数
int x = 5, y = 5;
std::cout << "整数比较: " << isEqual(x, y) << std::endl; // true
// Case2: 字符串
const char* str1 = "hello";
const char* str2 = "hello";
std::cout << "字符串比较: " << isEqual(str1, str2) << std::endl; // true//false 不确定, 因为是指针, 取决于编译器是否优化两个 str1 和 str2 指向同一内存地址, 我们这里需要显式具体化, 否则不保证"字符串相等"的语义
}
// 显式具体化 for const char*
template <>
bool isEqual<const char*>(const char* a, const char* b){
return std::strcmp(a, b) == 0;
}
- STL中的实际应用:
std::vector<bool>进行性能优化
2.2 类模板
- 定义了若干个类
- 显式模板实参指定 (C++17引入了类模板参数推导, 可以隐式指定)
- 可以带有多个参数, 可以带有普通参数
- 类模板中的静态成员属于实例化之后的类, 而不是模板本身
cpp
template <typename T, int size>
class Stack{
T buffer[size];
int top;
public:
void push(T x);
T pop();
};
template <typename T, int size>
void Stack<T, size>::push(T x){...}
template <typename T, int size>
T Stack<T, size>::pop(){...}
int main(){
Stack<int, 100> intStack;
Stack<double, 50> doubleStack;
}
C++ 中模板的完整定义通常放在头文件中
- 因为模板是在编译时实例化的, 编译器需要在编译阶段看到模板的完整定义, 才能生成相应的代码
- 如果模板定义放在源文件中, 编译器在编译其他源文件时无法访问模板定义, 导致链接错误
cpp
// file1.h
template <typename T>
class S{
T a;
public:
void f();
};
// file1.cpp
#include "file1.h"
template <typename T>
void S<T>::f(){...;}
template <class T>
T max(T a, T b){
return (a>b)? a:b;
}
void main(){
int a, b;
max(a,b);
S<int> x;
x.f();
}
// file2.cpp
extern double max(double, double); // 声明
void sub() {
double x = 1.1, y = 2.2;
double m = max(x, y); // 链接错误, 因为找不到 max 的定义
S<float> s; // 链接错误, 因为找不到 S<float> 的定义
s.f();
}
- 问题在于: 如果在模块A中要使用模块B中定义的某模板的实例,而在模块B中未使用这个实例,则模块A无法使用这个实例
if constexpr 解决编译问题:
问题代码:
cpp
template <typename T>
std::string autotoString(T value) {
if(std::is_same_v<T, std::string>) {
return value;
} else {
return std::to_string(value);
}
}
int main() {
std::string str = "Hello, World!";
std::cout << autotoString(str) << std::endl; // 错误! 因为 std::to_string(std::string) 不存在
}
解决方案, 使用 if constexpr:
cpp
template <typename T>
std::string autotoString(T value) {
if constexpr (std::is_same_v<T, std::string>) {
return value;
} else {
return std::to_string(value);
}
}
- 如果 T 是 std::string, 编译器直接删除 else 分支
- 如果 T 不是 std::string, 编译器直接删除 if 分支