【C++】模板进阶--详解 一. 非类型模板参数模板参数分两种模板参数分为类型形参与非类型形参。类型形参就是跟在class或typename后面的那个参数它代表一个类型。简单说就是“这个参数是一个类型”具体是什么类型等实例化的时候再确定。template class T class A; // T 是类型形参T 代表一个类型非类型形参就是用一个常量作为模板的参数在模板里面可以把这个参数当成常量来用。简单说就是“这个参数是一个值”不是一个类型。template class T, size_t N class A; // T 是类型形参N 是非类型形参1.1非类型模板参数基本用法非类型形参支持整型、枚举、指针、引用等类型templateint N, size_t M, bool Flag class A; // 整型常量 templatechar C class B; // 字符常量 templatesize_t N 10 class C; // 可以给默认值实例化的时候传的必须是编译期能确定的常量表达式A10, 20, true a1; // 传常量 const int n 5; An, 30, false a2; // const 变量也算常量表达式 int m 5; Am, 10, true a3; // 错误m 是变量不是常量表达式比如我开个栈templatesize_t N 10, bool flag false class Stack { private: int _a[N]; // N 作为数组大小编译期确定 int _top; };实例化方式Stack s0; // 使用默认值N 10 Stack5 s1; // N 5 Stack10, true s2; // N 10flag true非类型模板参数也支持默认值和类型参数一样不管哪种模板参数都可以给缺省值。1.2非类型模板参数的限制C20 之前非类型模板参数只支持整型int、char、size_t、bool 等枚举类型指针、引用C20 开始浮点数也可以作为非类型模板参数// C20 才支持 templatedouble D class A { // ... };注意非类型模板参数的值必须是编译期常量不能是运行期变量。1.3非类型模板参数与数组1.三种数组/容器的存储位置// 栈上分配 arrayint, 10 a1; // 40 字节栈上 arrayint, 100 a2; // 400 字节栈上 int a3[10]; // 40 字节栈上 // 堆上分配 vectorint v(100, 1); // 对象本身在栈上24字节数据在堆上400字节2.原生数组的越界问题int a3[10]; // 越界读不检查 cout a3[10] endl; // 越界了但程序不会报错输出内存中的随机值 // 越界写抽查不一定会报错 a3[12] 10; // 越界了但可能侥幸没崩溃 a3[20] 10; // 越界更多可能破坏其他数据或直接崩溃问题来了为什么是抽查原生数组越界属于未定义行为编译器不做检查。有时候越界写的是未使用的内存程序继续跑有时候写到了关键数据程序就崩了。这就是抽查——一般运气好没事运气不好就挂。3.array 的越界检查arrayint, 10 a1; a1[10]; // debug 下会检查越界直接断言报错 a1[12] 10; // debug 下同样检查4.array 和 vector 的内存占用cout sizeof(a2) endl; // 400 字节100 个 int × 4 字节 cout sizeof(v) endl; // 24 字节对象本身的大小array的 size 就是N × sizeof(T)数据直接放在对象里。vector的对象本身很小通常 24 字节数据在堆上单独分配。5.栈的生长方向void func() { int a 1; cout a endl; // 打印 func 中变量的地址 } int main() { int a 0; cout a endl; // 打印 main 中变量的地址 func(); return 0; }输出地址越来越小说明栈是向下生长的从高地址向低地址生长。注意1. 浮点数、类对象以及字符串是不允许作为非类2. 非类型的模板参数必须在编译期就能确认结果。二、模板的特化2.1 概念通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果需要特殊处理比如实现了一个专门用来进行小于比较的函数模板// 函数模板 -- 参数匹配 templateclass T bool Less(T left, T right) { return left right; } int main() { cout Less(1, 2) endl; // 可以比较结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout Less(d1, d2) endl; // 可以比较结果正确 Date* p1 d1; Date* p2 d2; cout Less(p1, p2) endl; // 可以比较结果错误 return 0; }可以看到Less绝对多数情况下都可以正常比较但是在特殊场景下就得到错误的结果。上述示例中p1指向的d1显然小于p2指向的d2对象但是Less内部并没有比较p1和p2指向的对象内容而比较的是p1和p2指针的地址这就无法达到预期而错误。此时就需要对模板进行特化。即在原模板类的基础上针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化2.2函数模板特化函数模板的特化步骤1. 必须要先有一个基础的函数模板2. 关键字template后面接一对空的尖括号3. 函数名后跟一对尖括号尖括号中指定需要特化的类型4. 函数形参表: 必须要和模板函数的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误。// 函数模板 -- 参数匹配 templateclass T //bool LessFunc(T const left, T const right) bool LessFunc(const T left, const T right) { return left right; } // 特化 //template //bool LessFuncconst Date*(const Date* const left, const Date* const right) //{ // return *left *right; //} // //template //bool LessFuncDate*(Date* const left, Date* const right) //{ // return *left *right; //} // 推荐 bool LessFunc(const Date* left, const Date* right) { return *left *right; } bool LessFunc(Date* left, Date* right) { return *left *right; } int main() { cout LessFunc(1, 2) endl; // 可以比较结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout LessFunc(d1, d2) endl; // 可以比较结果正确 Date* p1 d1; Date* p2 d2; cout LessFunc(p1, p2) endl; // 可以比较结果错误 const Date* p3 d1; const Date* p4 d2; cout LessFunc(p3, p4) endl; // 可以比较结果错误 const int i 0; int const j 0; const int rx i; int const ry i; return 0; }1.函数模板匹配的局限性函数模板可以对不同类型进行统一处理但有些类型用默认的比较方式会出错templateclass T bool LessFunc(const T left, const T right) { return left right; }当比较Date*指针时LessFunc比较的是指针地址的大小而不是Date对象本身的大小Date d1(2022, 7, 7); Date d2(2022, 7, 8); Date* p1 d1; Date* p2 d2; cout LessFunc(p1, p2) endl; // 比较的是 p1 和 p2 的地址值结果可能是错误的2.解决方法模板特化就是针对特定类型给模板提供一个专门的版本这叫模板特化// 特化专门为 Date* 类型提供实现 template bool LessFuncDate*(Date* const left, Date* const right) { return *left *right; // 比较 Date 对象本身 }然后现在调用LessFunc(p1, p2)就会走特化版本比较的是两个Date对象的大小。3.特化的写法分析template bool LessFuncDate*(Date* const left, Date* const right) { return *left *right; }template 空的尖括号表示这是一个全特化LessFuncDate* 指明特化的模板参数是 Date*Date* const 参数类型是主模板 const T 在 T Date* 时的展开形式4.const 的位置问题// 特化版本1匹配 T Date* template bool LessFuncDate*(Date* const left, Date* const right) // 特化版本2匹配 T const Date*不是同一个特化 template bool LessFuncconst Date*(const Date* const left, const Date* const right)Date*和const Date*是两种不同的类型因为特化时要保持一致。const的位置// const 在 * 右边 --- 修饰指针本身指针不能改但指向的数据能改 // 这是主模板 const T 在 T Date* 时的自然展开 Date* const left // const 在 * 左边 --- 修饰指向的数据数据不能改 // 这匹配的是 T const Date*而不是 T Date* const Date* left6.因此不推荐特化推荐重载特化的语法容易搞混const的位置更推荐直接用函数重载// 重载1处理 const Date* 类型 bool LessFunc(const Date* left, const Date* right) { return *left *right; } // 重载2处理 Date* 类型 bool LessFunc(Date* left, Date* right) { return *left *right; }举例用重载处理指针比较int main() { // 一些比较基础类型比较走主模板版本 cout LessFunc(1, 2) endl; // true没问题 // 比较 Date 对象走主模板版本 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout LessFunc(d1, d2) endl; // true没问题 // 比较 Date*走重载版本 Date* p1 d1; Date* p2 d2; cout LessFunc(p1, p2) endl; // true比较的是 Date 对象 // 比较 const Date*走重载版本 const Date* p3 d1; const Date* p4 d2; cout LessFunc(p3, p4) endl; // true比较的是 Date 对象 // const 的位置等价写法 const int i 0; int const j 0; // 和 const int 等价 const int rx i; int const ry i; // 和 const int 等价 return 0; }【补充】const 与指针的位置关系给一个口诀方便记忆const 在*左边修饰的是指向的数据const 在*右边修饰的是指针本身。表格理解写法读法const 修饰谁数据能否改指针本身能否改说明const Date* p指向 const Date 的指针数据*p不能能指向的数据是只读的指针本身可以指向别处Date const* p同上数据*p不能能和const Date*完全等价写法不同而已Date* const p指向 Date 的 const 指针指针本身p能不能指针地址固定但指向的数据可以修改const Date* const p指向 const Date 的 const 指针数据 指针不能不能数据和指针都不能改完全只读Date const* const p同上数据 指针不能不能和上面等价const Date* p指向 const Date 的指针的引用数据*p不能能通过引用可以改指针指向引用本身不可改但可以改变引用指向的指针引用的特性Date* const p指向 Date 的 const 指针的引用指针本身p能不能通过引用也不能改指针相当于给Date* const起了一个别名2.3 类模板特化类模板特化步骤必须要先有一个基础的类模板。关键字 template 后面接一对空的尖括号 类名后跟一对尖括号 尖括号中指定需要特化的类型。类模板的特化分为全特化和偏特化。2.3.1 全特化全特化即是将模板参数列表中所有的参数都确定化。templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 endl; } private: T1 _d1; T2 _d2; }; // 全特化 template class Dataint, char { public: Data() { cout Dataint, char endl; } }; void TestVector() { Dataint, int d1;//基础类模板 Dataint, char d2; }2.3.2 偏特化偏特化任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类templateclass T1, class T2 class Data { public: Data() {coutDataT1, T2 endl;} private: T1 _d1; T2 _d2; };偏特化有以下两种表现方式部分特化将模板参数类表中的一部分参数特化。// 将第二个参数特化为int template class T1 class DataT1, int { public: Data() {coutDataT1, int endl;} private: T1 _d1; int _d2; };参数更进一步的限制偏特化并不仅仅是指特化部分参数而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。//两个参数偏特化为指针类型 template typename T1, typename T2 class Data T1*, T2* { public: Data() {coutDataT1*, T2* endl;} private: T1 _d1; T2 _d2; }; //两个参数偏特化为引用类型 template typename T1, typename T2 class Data T1, T2 { public: Data(const T1 d1, const T2 d2) : _d1(d1) , _d2(d2) { coutDataT1, T2 endl; } private: const T1 _d1; const T2 _d2; }; void test2 () { Datadouble , int d1; // 调用特化的int版本 Dataint , double d2; // 调用基础的模板 Dataint *, int* d3; // 调用特化的指针版本 Dataint, int d4(1, 2); // 调用特化的指针版本 }2.3.3 类模板特化应用示例有如下专门用来按照小于比较的类模板Less#includevector #includealgorithm templateclass T struct Less { bool operator()(const T x, const T y) const { return x y; } }; int main() { Date d1(2022, 7, 7); Date d2(2022, 7, 6); Date d3(2022, 7, 8); vectorDate v1; v1.push_back(d1); v1.push_back(d2); v1.push_back(d3); // 可以直接排序结果是日期升序 sort(v1.begin(), v1.end(), LessDate()); vectorDate* v2; v2.push_back(d1); v2.push_back(d2); v2.push_back(d3); // 可以直接排序结果错误日期还不是升序而v2中放的地址是升序 // 此处需要在排序过程中让sort比较v2中存放地址指向的日期对象 // 但是走Less模板sort在排序时实际比较的是v2中指针的地址因此无法达到预期 sort(v2.begin(), v2.end(), LessDate*()); return 0; }通过观察上述程序的结果发现对于日期对象可以直接排序并且结果是正确的。但是如果待排序元素是指针结果就不一定正确。因为sort最终按照Less模板中方式比较所以只会比较指针而不是比较指针指向空间中内容此时可以使用类版本特化来处理上述问题// 对Less类模板按照指针方式特化 template struct LessDate* { bool operator()(Date* x, Date* y) const { return *x *y; } }完整代码如下#include iostream #include vector #include algorithm using namespace std; class Date { public: Date(int year 1, int month 1, int day 1) : _year(year), _month(month), _day(day) {} // 重载 operator bool operator(const Date d) const { if (_year ! d._year) return _year d._year; if (_month ! d._month) return _month d._month; return _day d._day; } /成员函数打印 void print(ostream out cout) const { out _year - _month - _day; } private: int _year; int _month; int _day; }; // Less 仿函数 templateclass T struct Less { bool operator()(const T x, const T y) const { return x y; } }; // 特化版本针对 Date* template struct LessDate* { bool operator()(Date* x, Date* y) const { return *x *y; } }; // 测试 int main() { Date d1(2022, 7, 7); Date d2(2022, 7, 6); Date d3(2022, 7, 8); // 测试 vectorDate* vectorDate* v2; v2.push_back(d1); v2.push_back(d2); v2.push_back(d3); cout 排序前: ; for (auto p : v2) { p-print(); // 调用 print cout ; } cout endl; sort(v2.begin(), v2.end(), LessDate*()); cout 排序后: ; for (auto p : v2) { p-print(); // 调用 print cout ; } cout endl; return 0; }输出打印仿函数就是一个类里面重载了()让对象能像函数一样被调用传给sort、set这些容器告诉它们怎么比大小。比普通函数好用因为可以带状态。长得像函数其实是个对象三. 模板分离编译3.1 什么是分离编译一个程序项目由若干个源文件共同实现而每个源文件单独编译生成目标文件最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。模板不能把声明放 .h定义放 .cpp。因为编译的时候各编各的.cpp 不知道你要用啥类型就不生成代码.h 那边要用但找不到定义最后链接就报错。解决办法直接把定义也写在 .h 里别分开。3.2 模板的分离编译假如有以下场景模板的声明与定义分离开在头文件中进行声明源文件中完成定义// a.h templateclass T T Add(const T left, const T right); // a.cpp templateclass T T Add(const T left, const T right) { return left right; } // main.cpp #includea.h int main() { Add(1, 2); Add(1.0, 2.0); return 0; }板分离编译总结1.编译链接的四个步骤.cpp --预处理 -- .i -- 编译 --.s → 汇编 -- .o -- 链接 -- .exe步骤干啥的预处理头文件展开、宏替换、去注释编译检查语法生成汇编代码汇编汇编代码转成二进制机器码链接多个 .o 文件拼成可执行文件把函数地址对上2.模板分离编译为啥会报错模板的声明放 .h定义放 .cpp 时1.实例化需要完整的定义光有声明不够。// a.h 里只有这个 templateclass T T Add(const T left, const T right); // 只有声明 // main.cpp 里调用 Add(1, 2); // 编译器看到这个调用知道需要实例化 Addint // 但它找不到 Addint 的定义函数体 // 所以没法实例化只能留个标记让链接器去找2.编译链接全过程你的情况编译 a.cpp编译器看到模板定义如果写在了 a.cpp 里但没人调用它所以不生成任何机器码a.obj 是空的没有 Add 相关的代码编译 main.cpp看到Add(1, 2)需要Addint但只有声明找不到定义编译器无法实例化在 main.obj 留了个标记我需要Addint链接时链接器去 a.obj 里找Addinta.obj 是空的报错找不到符号3.3 解决方法1. 将声明和定义放到一个文件 xxx.hpp 里面或者xxx.h其实也是可以的。推荐使用这种。2. 模板定义的位置显式实例化。这种方法不实用不推荐使用。【分离编译扩展阅读】为什么C编译器不能支持对模板的分离式编译-CSDN博客四. 模板总结【优点】1. 模板复用了代码节省资源更快的迭代开发C的标准模板库(STL)因此而产生2. 增强了代码的灵活性【缺陷】1. 模板会导致代码膨胀问题也会导致编译时间变长2. 出现模板编译错误时错误信息非常凌乱不易定位错误