)
欢迎阅读 欢迎来到「C 模板进阶」深入讲解本文将带你从“基础模板的局限性”这一痛点出发逐步深入理解非类型模板参数、模板特化全特化与偏特化、模板分离编译的注意事项、typename 在嵌套类型中的使用让你对 C 模板机制有更全面、更深刻的认知。在开始之前建议你先了解背景知识在掌握了基础模板函数模板与类模板的语法与使用后我们会发现某些特殊场景下通用模板可能产生错误或不合理的结果——比如用Less比较两个指针时比较的是指针地址而非指向的内容。此外模板的编译链接方式与普通函数/类也有显著差异。这些进阶知识点是写出高质量泛型代码的必经之路。明确学习目标掌握以下五个核心知识板块——①非类型模板参数理解模板参数分为类型形参class/typename与非类型形参常量表达式如size_t N两类掌握非类型模板参数在定义静态数组等场景中的应用明确其值必须在编译期确定的约束②模板特化理解特化的本质——针对某些特定类型提供不同于通用模板的特殊实现。掌握函数模板特化的写法如template bool LessDate*(Date* left, Date* right)以及类模板的全特化所有模板参数都被确定与偏特化部分参数确定或对参数进行进一步限制如指针/引用版本的区别与使用场景③模板分离编译理解模板不支持将声明与定义分离在不同的.h和.cpp文件中的原因模板在编译期实例化需要看到完整定义才能生成代码并明确推荐将模板的声明与定义统一放在头文件中的工程实践④typename 关键字在模板中的关键作用理解当编译器遇到T::iterator这种依赖模板参数的嵌套类型时无法区分它是类型还是静态成员变量必须使用typename显式告知编译器“这是一个类型”避免编译报错⑤模板与普通函数的重载解析规则掌握当模板函数与普通非模板函数同时存在且参数匹配时编译器优先选择普通函数的行为了解如何通过空模板实参列表foo(42)或显式指定模板参数fooint(42)强制调用模板版本。一.非类型模板参数模板参数分为类型形参与非类型形参两大类。类型形参即出现在模板参数列表中跟在class或typename后面的参数名称它代表一种尚未指定的具体类型在模板实例化时由用户传入。非类型形参则是用一个常量作为类模板或函数模板的一个参数在模板内部该参数可以被当作常量来使用其值在编译期就必须确定。namespace bite { // 定义一个模板类型的静态数组 templateclass T, size_t N 10 // 类型形参 非类型形参 }浮点数、类对象以及字符串字面量C20标准前不允许作为非类型模板参数。非类型模板参数的值必须在编译期就能确定结果。模板参数也可以指定默认值但必须遵守“从右向左”依次提供的规则。二.模板的特化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 函数模板特化// 基础的函数模板 -- 参数匹配 templateclass T bool Less(T left, T right) { return left right; } // 对Less函数模板进行特化 template bool LessDate*(Date* left, Date* 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; }必须要先有一个基础的函数模板关键字template后面接一对空的尖括号函数名后跟一对尖括号尖括号中指定需要特化的类型2.3 类模板特化必须要先有一个基础的类模板关键字template后面接一对尖括号类名后跟一对尖括号尖括号中指定需要特化的类型全特化全特化即是将模板参数列表中所有的参数都确定化templateclass T1, class T2 class Data { public: Data() {coutDataT1, T2 endl;} private: T1 _d1; T2 _d2; }; template class Dataint, char { public: Data() {coutDataint, char endl;} private: int _d1; char _d2; };偏特化的两种表示形式偏特化针对模版参数进一步条件限制设计的特化版本//基础的类模板 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* { private: T1 _d1; T2 _d2; } //两个参数偏特化为引用类型 template typename T1, typename T2 class Data T1, T2 { private: const T1 _d1; const T2 _d2; }; void test2 () { Datadouble , int d1; // 调用特化的int版本 Dataint , double d2; // 调用基础的模板 Dataint *, int* d3; // 调用特化的指针版本 Dataint, int d4(1, 2); // 调用特化的指针版本 }三.模板分离编译关于这部分内容简单来说就是不推荐将模板的声明与定义分离在不同的文件中。至于具体原因涉及编译链接的复杂机制这里不展开详述感兴趣的可以自行查阅相关资料。我这里只给出结论不推荐分离因为在实际开发中实用性不高且维护成本较大。四.模板补充如果使用模板参数中的类型或者未实例化的模板类作为迭代器类型编译器可能会报错这是因为当编译器解析模板代码时遇到依赖模板参数的嵌套类型如T::iterator它无法确定iterator到底是一个类型还是一个静态成员变量——因为访问静态成员变量的写法也是T::iterator。编译器默认会将其视为静态成员或变量而不是类型从而导致编译错误。此时只需要在类型前面加上typename关键字即可显式告诉编译器“这是一个类型”从而消除歧义。对于模板模板参数中的类名也可以用class来声明。typename continer :: interator it v.begin()当存在模板函数和普通非模板函数重载时会优先使用普通函数// (1) 普通函数非模板 void foo(int x) { std::cout 普通函数: foo(int)\n; } // (2) 函数模板 templatetypename T void foo(T x) { std::cout 模板函数: foo(T)\n; } int main() { foo(42); // 调用第二个 }注意非模板函数 模板特化 模板泛化如果不想调用非模板函数那么就需要用标明我要调用的是模板foo(42); // 空模板实参列表 → 强制走模板 // 或 fooint(42); // 显式指定模板参数 闭幕 恭喜你完成了C 模板进阶的学习今天我们系统梳理了非类型模板参数、模板特化函数特化与类特化、模板分离编译以及typename 与模板补充知识这四大核心模块。为了巩固知识并进一步拓展建议你动手实践在本地 IDE 或在线编译器中编写代码尝试不同的测试用例亲身体验每个知识点的用法。深入思考非类型模板参数非类型模板参数有哪些限制例如可以是int但不能是double为什么非类型模板参数必须是编译期常量函数模板特化函数模板特化和函数重载的区别是什么什么情况下必须用特化而不是重载类模板特化全特化与偏特化全特化和偏特化的语法区别是什么偏特化有哪几种形式部分参数特化、指针特化、引用特化模板分离编译为什么不推荐将模板的声明和定义分离在不同文件中模板的实例化发生在编译期还是链接期typename 关键字为什么在模板中访问依赖类型的嵌套类型如T::iterator时必须加typename不加会导致什么问题如果你觉得本文对你有所帮助欢迎点赞 / 收藏关注作者获取更多题解留言交流你的疑问或优化思路深入思考参考答案非类型模板参数非类型模板参数只能是整型、枚举、指针、引用等不能是浮点数、字符串或类类型。因为模板实例化在编译期完成非类型模板参数的值必须在编译期确定而浮点数在编译期可能存在精度问题所以标准禁止使用。函数模板特化函数模板特化是针对特定类型提供特殊实现函数重载是定义同名不同参的普通函数。当需要对某个特定类型进行特殊处理时用特化当参数个数或类型不同时需要新功能时用重载。特化不能改变函数参数个数重载可以。类模板特化全特化与偏特化全特化是所有模板参数都指定具体类型语法为template class Dataint, char{};。偏特化是部分参数指定或对参数进行条件限制包括部分参数特化templateclass T class DataT, int{};、指针特化templateclass T1, class T2 class DataT1*, T2*{};、引用特化templateclass T1, class T2 class DataT1, T2{};。模板分离编译模板的实例化发生在编译期编译器需要看到完整的模板定义才能生成代码。如果将声明放在.h定义放在.cpp在链接时其他文件找不到实例化后的代码导致链接错误。推荐将模板的声明和定义都放在.h文件中。typename 关键字模板中T::iterator这种写法有歧义编译器默认将其视为静态成员变量而非类型。加上typename明确告诉编译器这是一个类型避免编译错误。不加会导致编译失败。