• C++要笑着学:非类型模板参数 | 模板的特化


       C++ 表情包趣味教程 👉 《C++要笑着学》

    💭 写在前面

    我们之前讲过C++的模板,考虑到当时还没有将 STL,所以并没有一次性讲完,我们把剩余的部分放到了讲完部分 STL 容器的后面去讲,这样比较方去讲解。比如我们本章我们会通过 STL 的 array 去讲解非类型模板参数。本章还会重点讲解模板的特化,最后简单的探讨一下C++引入模板的优缺点。

     本篇博客全站热榜排名:未上榜


    Ⅰ. 非类型模板参数(Nontype Template Parameters)

    0x00 引入:什么是非类型模板参数?

    " 对于函数模板和类模板,模板参数并不局限于类型,普通值也可以作为模板参数 "

      STL 的 array 就有一个非类型模板参数。

    注意看,我们普通定义的 T 是类型,而 N 这里并不是类型,而是一个常量!

    类型模板参数定义的是虚拟类型,注重的是你要传什么,而 非类型模板参数定义的是常量。

    1. "非类型模板参数"
    2. 👇
    3. template<class T, size_t N> class array;
    4. 👆
    5. "类型模板参数"

    0x01 非类型模板参数的使用场景

    💭 举例:假设我们要定义一个静态栈: 

    1. #define N 100
    2. template<class T>
    3. class Stack {
    4. private:
    5. int _arr[N];
    6. int _top;
    7. };

    ❓ 思考:我现在想定义两个容量不一样的栈,一个容量是100 另一个是 500,能做到吗?

    这就像 typedef 做不到一个存 int 一个存 double,而使用模板可以做到 st1intst2 double

    这里你的 #define 无论是改 100 还是改 500 都没办法解决这里的问题,

    对应的,这里使用非类型模板参数就可以做到 s1 存 100,s2 存 500。

    💡 解决方案:定义一个常量 N

    1. // #define N 100
    2. template<class T, size_t N>
    3. class Stack {
    4. private:
    5. int _arr[N];
    6. int _top;
    7. };
    8. int main(void)
    9. {
    10. Stack<int, 100> st1; // 我期望它的大小是100
    11. Stack<double, 500> st2; // 我期望它的大小是500
    12. return 0;
    13. }

    这里我们在模板这定义一个常量 N,派遣它去做数组的大小。

    于是我们就可以在实例化 Stack 的时候指定其实例化对象的大小了,分别传 100 和 500。

     这比宏更爽!它能传一个常量过去,定义不同的对象可以传不同的常量过去。

    0x02 一些值得注意的点

    📌 注意事项 ①:非类型模板参数是是常量,是不能修改的。

    1. template<class T, size_t N>
    2. class Stack {
    3. public:
    4. void f() { // 修改常量试试看
    5. N = 10;
    6. }
    7. private:
    8. int _arr[N];
    9. int _top;
    10. };
    11. int main(void)
    12. {
    13. Stack<int, 100> st1;
    14. st1.f();
    15. return 0;
    16. }

    🚩 运行结果:(报错)

    test1711.cpp:10:15: error: lvalue required as left operand of assignment
                 N = 10;
                 ~~^~~~

    📌 注意事项②:有些类型是不能作为非类型模板参数的,比如浮点数、类对象以及字符串。

    非类型模板参数基本上都是整型,实际上也只有整型是有意义和价值的(可以这么理解)。

    (char 也算整型,只不过是一个字节的整型,你不能因为它一个字节就歧视它)

    📌 注意事项③:非类型的模板参数必须在编译期就能确认结果。

    0x03 顺便介绍一下 STL 的 array

     " 我们对 STL 的 array 安排的场次如此潦草,在这里穿插下随便讲讲好啦 "

    🔍 官方文档:array - C++ Reference

    现在学了非类型模板参数了,我们现在再来回头看 array

    array 是 C++11 新增的,它有什么独特的地方吗?

     很可惜,基本没有,并且 vector 可以完全碾压 array……

    而且就算说它有,那也不是优势反而是劣势,这就是为什么我们没有这么积极的讲解 array

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int main(void)
    6. {
    7. vector<int> v1(100, 0);
    8. array<int, 100> a1;
    9. cout << "size of v1: " << sizeof(v1) << endl;
    10. cout << "size of a1: " << sizeof(a1) << endl;
    11. return 0;
    12. }

    🚩 运行结果:

    vector 是开在空间大的堆上的而 array 是开在寸土寸金的栈上的,堆可比栈的空间大太多太多了。

    最尴尬的是 array 能做的操作几乎 vector 都能做,因为 vector 的存在 array 显得有些一无是处。

     所以我们拿 array 去对标 vector 是不对的,拿去和原生数组比还是可以对比的。

    但是 array 也只是封装过的原生数组罢了,是真的菜:

    1. array<int, 100> a1; // 封装过的原生数组
    2. int a2[100]; // 原生数组

    比起原生数组,array 的最大优势也只是有一个越界的检查,读和写都可以检查到是否越界。

    要对比的话也只能欺负一下原生数组,然而面对强大的 vectorarray 完全没有招架之力。

    如何评价 array?在 C++11 增加完 array 后备受吐槽,从简化的角度来说完全可以不增加 array

     我的评价是 —— 

    " 十年磨一剑,但磨的是十年前的技术 "

    🔺 总结:array 相较于原生数组,有越界检查之优势,实际中建议直接用 vector。

    Ⅱ. 模板的特化(Template Specialization)

    0x01 引入:给特殊类型准备特殊模板

     通常情况下,使用模板可以实现一些与类型无关的代码

    但是,对于一些特殊类型,可能我们就要对其进行一些 "特殊的处理" 。

    💭 举例:如果不对特殊类型进行特殊处理就可能会出现一些问题,比如:

    1. #include
    2. #include "Date.h" /* 引入自己实现的日期类 */
    3. using namespace std;
    4. /* 判断左数是否比小于右数 */
    5. template<class T>
    6. bool Less(T left, T right) {
    7. return left < right;
    8. }
    9. int main(void)
    10. {
    11. cout << Less(1, 2) << endl; // 可以比较,结果正确
    12. Date d1(2022, 7, 7);
    13. Date d2(2022, 7, 8);
    14. cout << Less(d1, d2) << endl; // 可以比较,结果正确
    15. Date* p1 = new Date(2022, 7, 16);
    16. Date* p2 = new Date(2022, 7, 15);
    17. cout << Less(p1, p2) << endl; // 可以比较,结果正确
    18. return 0;
    19. }
    ❓ 运行结果:(我们运行几次发现,其结果不稳定,对于 Date* 一会是真一会是假)

    问题出在没传指针,传的是 p1p2,这里传 *p1 *p2 就能解决。但是……

     这时候冲出个土匪拿炸弹强迫你:不让你传星号,就必须传 p1 p2,怎么办?

    不慌,我们还可以用一种特殊的方式 —— 模板的特化针对某些类型要进行特殊化处理

    0x01 模板特化的步骤

     首先,必须要先有一个基础的函数模板。

     其次,关键字 template 后面接上一对空的 <> 尖括号。

     然后,函数名后跟一对尖括号,尖括号中指定需要特化的内容。

     最后,函数形参表必须要和模板函数的基础参数类型完全相同,不同编译器会恶心人。

    💬 代码演示:模板的特化

    1. #include
    2. #include "Date.h"
    3. using namespace std;
    4. // 必不可少的原本
    5. template<class T>
    6. bool Less(T left, T right) {
    7. return left < right;
    8. }
    9. // 针对某些类型要特殊化处理 ———— 使用模板的特化解决
    10. template<>
    11. bool Less(Date* left, Date* right) {
    12. return *left < *right;
    13. }
    14. int main(void)
    15. {
    16. cout << Less(1, 2) << endl; // 可以比较,结果正确
    17. Date d1(2022, 7, 7);
    18. Date d2(2022, 7, 8);
    19. cout << Less(d1, d2) << endl; // 可以比较,结果正确
    20. Date* p1 = new Date(2022, 7, 16);
    21. Date* p2 = new Date(2022, 7, 15);
    22. cout << Less(p1, p2) << endl; // 可以比较,结果正确
    23. return 0;
    24. }

    🚩 运行结果:(运行多次后发现结果正常,每次都一致)

    💡 解读:对于普通类型,它还是会调正常的模板。对于 Date* 编译器就会发现这里有个专门为 Date* 而准备的特化版本,编译器会优先选择该特化版本。这,就是模板的特化。

     当然了,函数内你可以自己设计如何去处理你想要处理的特殊情况,这都是你说的算:

    1. template<>
    2. bool Less(Date* left, Date* right) {
    3. // 这里面你可以做各种处理
    4. }

    ❓ 思考:现在我们加一个普通函数,Date* 会走哪个版本?

    1. // 原模板
    2. template<class T>
    3. bool Less(T left, T right) {
    4. return left < right;
    5. }
    6. // 对模板特化的
    7. template<>
    8. bool Less(Date* left, Date* right) {
    9. return *left < *right;
    10. }
    11. // 直接匹配的普通函数
    12. bool Less(Date* left, Date* right) {
    13. return *left < *right;
    14. }

    🔑 答案:函数重载,会走直接匹配的普通函数版本,因为是现成的,不用实例化。你可以这么理解:原模板是生肉,模板特化是半生不熟的肉,直接匹配的普通函数是熟肉。

    🔺 结论:函数模板不一定非要特化,因为在参数里面就可以处理,写一个匹配参数的普通函数也更容易理解。

    0x02 类模板的特化

     刚才函数模板不一定非要特化,因为可以写一个具体实现的函数。

    但是类模板我们没法实现一个具体的实际类型,就必须要特化了。

    💭 比如这里 d2 需要接收一个 int 和 一个 double 的值:

    1. #include
    2. #include "Date.h"
    3. using namespace std;
    4. template<class T1, class T2>
    5. class Data {
    6. public:
    7. Data() {
    8. cout << "Data" << endl;
    9. }
    10. private:
    11. T1 _d1;
    12. T2 _d2;
    13. };
    14. int main(void)
    15. {
    16. Data<int, int> d1;
    17. Data<int, double> d2; // 需要特化解决
    18. return 0;
    19. }

    💬 代码演示:类模板的特化

    1. template<class T1, class T2>
    2. class Data {
    3. public:
    4. Data() {
    5. cout << "Data" << endl;
    6. }
    7. private:
    8. T1 _d1;
    9. T2 _d2;
    10. };
    11. // 类模板的特化
    12. template<>
    13. class Data<int, double> {
    14. public:
    15. Data() {
    16. cout << "Data" << endl;
    17. }
    18. };
    19. int main(void)
    20. {
    21. Data<int, int> d1;
    22. Data<int, double> d2;
    23. return 0;
    24. }

    🚩 运行结果:

    0x03 全特化和半特化

     全特化:全特化即是将模板参数列表中所有的参数都确定化。

    (我们刚才写的就是全特化)

    1. ...
    2. // 全特化
    3. template<>
    4. class Data<int, double> {
    5. public:
    6. Data() {
    7. cout << "Data" << endl;
    8. }
    9. };

     半特化(又称偏特化):将部分参数类表中的一部分参数特化。

    (半特化并不是特化一半,就像半缺省并不是缺省一半一样)

    1. ...
    2. // 半特化(偏特化)
    3. template<class T1>
    4. class Datachar> {
    5. public:
    6. Data() {
    7. cout << "Data" << endl;
    8. }
    9. };
    10. int main(void)
    11. {
    12. // 只要第二个值是 char 都会匹配到半特化
    13. Data<int, char> d3;
    14. Data<char, char> d4;
    15. return 0;
    16. }

     半特化还有一种表现方式,半特化可以用来对参数进行更进一步的限制。

    💬 代码演示:限制两个参数都是指针

    1. ...
    2. // 半特化还可以对参数进行进一步限制
    3. template<class T1, class T2>
    4. class Data {
    5. public:
    6. Data() {
    7. cout << "Data" << endl;
    8. }
    9. };
    10. int main(void)
    11. {
    12. // 只要你两个参数都是指针,就匹配
    13. Data<int*, char*> d5;
    14. Data<char*, string*> d6;
    15. Data<char**, void*> d7;
    16. return 0;
    17. }

    🚩 运行结果:

    💬 代码演示:限制两个参数都是引用

    1. template<class T1, class T2>
    2. class Data {
    3. public:
    4. Data() {
    5. cout << "Data" << endl;
    6. }
    7. };
    8. int main(void)
    9. {
    10. // 只要你两个参数都是引用,就匹配
    11. Data<int&, int&> d8;
    12. Data<char&, string&> d9;
    13. return 0;
    14. }

    🚩 运行结果:

    Ⅲ. 模板的优缺点

    0x00 优点

    ① 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。

    ② 增强了代码的灵活性。

    0x01 缺点

    ① 模板会导致代码膨胀问题,也会导致编译时间变长。

    ② 出现模板编译错误时,错误信息非常凌乱,不易定位错误。


    1. 📌 [ 笔者 ]   王亦优
    2. 📃 [ 更新 ]   2022.8.1
    3. ❌ [ 勘误 ]   /* 暂无 */
    4. 📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
    5. 本人也很想知道这些错误,恳望读者批评指正!

    📜 参考资料 

    C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

    Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

    百度百科[EB/OL]. []. https://baike.baidu.com/.

    比特科技. C++[EB/OL]. 2021[2021.8.31]. 

  • 相关阅读:
    Mobile-Former: Bridging MobileNet and Transformer详解
    访问nginx报错502日志:failed (13: Permission denied) while connecting to upstream
    stack-es-标准篇-ElasticsearchClient-match_phrase_prefix
    爬虫日常实战
    Python基础教程:行与缩进正确用法教程
    【Linux系统编程:基础IO 下】dup2 实现输出重定向、输入重定向、追加重定向 | 理解磁盘 | 理解文件系统中inode的概念 | 软硬链接
    flume入门案例
    C语言试题124之给一个不多于 5 位的正整数,要求:一、求它是几位数,二、逆序打印出各位数字
    【PAT(甲级)】1041 Be Unique
    ansible ---- ansible.builtin.command
  • 原文地址:https://blog.csdn.net/weixin_50502862/article/details/125993278