• 【C++】模版进阶


     我们在之前的博客中讲述过模版的使用:【C++】模版初阶,但这只是模版最基本的使用,下面再深入模版,看看还有另外什么用法:

    目录

    一、非类型模板参数

    二、模板的特化

    2.1 什么是模版的特化

    2.2 函数模版的特化

    2.3 类模版的特化

    2.3.1 全特化

    2.3.2 偏特化

    三、模板的分离编译

    3.1 解决方法


    一、非类型模板参数

    我们知道模版可以传递类型参数,除此之外还可以传递非类型参数:

    例如:

    1. #define N 10
    2. template<class T>
    3. struct Static_sequence
    4. {
    5. T a[N];
    6. };

    上诉代码可以建立一个任意类型的静态顺序表,但是N值是确定的,我们如何建立我们想要的大小的静态顺序表呢?

    再来看看下面的代码:

    1. template<class T, size_t N = 10>
    2. struct Static_sequence
    3. {
    4. T a[N];
    5. };

    我们在模版上再加一个非类型参数size_t N,这个参数可以传入我们需要的值来初始化静态顺序表:

    1. int main()
    2. {
    3. Static_sequence<int, 20> q;
    4. return 0;
    5. }

    在上述代码中我们传入了一个20,可以看到开辟了一个20个元素的静态数组:

    对于该非类型模板参数,我们只能设定整型家族(int/char/short/...),对于其他类型是不支持的,例如:

    对于设定的非类型模板参数,系统是将其作为常量看待的,所以它可以初始化数组元素个数

    例如我们想要对传入的值做修改:

    编译器是不能通过的

    二、模板的特化

    2.1 什么是模版的特化

    通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:指针类型的比较:

    1. class Date
    2. {
    3. friend ostream& operator<<(ostream& out, const Date& d);
    4. public:
    5. Date(int year, int month, int day)//构造函数
    6. {
    7. if (month < 1 || month>12 || (day<1 || day>GetMonthDay(year, month)))//判断日期是否合法
    8. {
    9. cout << "Illegal date!" << endl;
    10. }
    11. else
    12. {
    13. _year = year;
    14. _month = month;
    15. _day = day;
    16. }
    17. }
    18. int GetMonthDay(int year, int month) const//获取year年month月的天数
    19. {
    20. int MonthDay[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };//用数组来依次存储平年1到12月的天数
    21. if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))//判断是否为闰年的2月
    22. {
    23. return 29;
    24. }
    25. return MonthDay[month - 1];
    26. }
    27. //运算符重载
    28. bool operator==(const Date& d) const//判断日期是否相同
    29. {
    30. return _year == d._year && _month == d._month && _day == d._day;
    31. }
    32. bool operator!=(const Date& d) const//判断日期是否不相同
    33. {
    34. return !(*this == d);
    35. }
    36. bool operator<(const Date& d) const//判断当前日期是否在传入日期之前
    37. {
    38. return _year < d._year || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day);
    39. }
    40. bool operator<=(const Date& d) const//判断当前日期是否在传入日期之前或相同
    41. {
    42. return (*this < d) || (*this == d);
    43. }
    44. bool operator>(const Date& d) const//判断当前日期是否在传入日期之后
    45. {
    46. return !(*this <= d);
    47. }
    48. bool operator>=(const Date& d) const//判断当前日期是否在传入日期之后或相同
    49. {
    50. return !(*this < d);
    51. }
    52. private:
    53. int _year;
    54. int _month;
    55. int _day;
    56. };
    57. ostream& operator<<(ostream& out, const Date& d)
    58. {
    59. out << d._year << "/" << d._month << "/" << d._day << endl;
    60. return out;
    61. }
    62. // 函数模板 -- 参数匹配
    63. template<class T>
    64. bool Less(const T x, const T y)
    65. {
    66. return x < y;
    67. }
    68. int main()
    69. {
    70. std::cout << Less(Date(2022, 9, 27), Date(2022, 9, 25)) << endl;//Date类型比较
    71. std::cout << Less(new Date(2022, 10, 27), new Date(2022, 10, 25)) << endl;//Date指针类型比较
    72. return 0;
    73. }

    比较结果:

    因为我们在Date类中运算符重载了<的比较方法,所以对于Date类可以直接调用该方法进行比较,但是对于Date*类型,模版找不到比较方法,只能默认比较地址空间的大小,导致了结果的错误

    为了解决该问题,我们引入模版的特化:

    2.2 函数模版的特化

    函数模板的特化步骤:

    1.必须要先有一个基础的函数模板

    2.关键字template后面接一对空的尖括号<>

    3.函数名后跟一对尖括号,尖括号中指定需要特化的类型

    4.函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误

    下面我们对上面的模版函数进行指针的特例化:

    1. // 函数模板 -- 参数匹配
    2. template<class T>
    3. bool Less(const T x, const T y)
    4. {
    5. return x < y;
    6. }
    7. // 对Less函数模板进行特化
    8. template<>
    9. bool Less(Date* x, Date* y)
    10. {
    11. return *x < *y;
    12. }

    这下结果就正确了: 

    该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时要特别给出,因此函数模板不建议特化,平时我们使用实例化解决即可

    2.3 类模版的特化

    2.3.1 全特化

    我们来设置一个类模版:

    1. // 类模板 -- 参数匹配
    2. template<class T>
    3. class Less
    4. {
    5. bool operator()(T& x, T& y)
    6. {
    7. return x < y;
    8. }
    9. };
    10. //全特化
    11. template<>
    12. class Less
    13. {
    14. bool operator()(Date* x, Date* y)
    15. {
    16. return *x < *y;
    17. }
    18. };

    全特化即是将模板参数列表中所有的参数都确定化,在上述代码中我们将T类型指明为Date*类型,这种将模板参数列表全部指明为一个确定的类型的操作就是全特化

    2.3.2 偏特化

    偏特化就是任何针对模版参数进一步进行条件限制设计的特化版本

    如下:我们将类模板参数进一步限制为指针类型,但具体为上面类型的指针并未说明:

    1. // 类模板 -- 参数匹配
    2. template<class T>
    3. class Less
    4. {
    5. bool operator()(T& x, T& y)
    6. {
    7. return x < y;
    8. }
    9. };
    10. //偏特化,对模版参数进一步进行条件限制
    11. template<class T>
    12. class Less
    13. {
    14. bool operator()(T* x, T* y)
    15. {
    16. return *x < *y;
    17. }
    18. };
    1. // 类模板 -- 参数匹配
    2. template<class T1, class T2>
    3. class Data
    4. {
    5. public:
    6. Data() { cout << "Data" << endl; }
    7. private:
    8. T1 _d1;
    9. T2 _d2;
    10. };
    11. //两个参数偏特化为指针类型
    12. template <typename T1, typename T2>
    13. class Data
    14. {
    15. public:
    16. Data() { cout << "Data" << endl; }
    17. private:
    18. T1 _d1;
    19. T2 _d2;
    20. };
    21. //两个参数偏特化为引用类型
    22. template <typename T1, typename T2>
    23. class Data
    24. {
    25. public:
    26. Data(const T1& d1, const T2& d2)//参数偏特化为引用类型时一定要初始化
    27. : _d1(d1)
    28. , _d2(d2)
    29. {
    30. cout << "Data" << endl;
    31. }
    32. private:
    33. const T1& _d1;
    34. const T2& _d2;
    35. };
    36. //一个参数偏特化为引用类型,一个参数偏特化为指针类型
    37. template <typename T1, typename T2>
    38. class Data
    39. {
    40. public:
    41. Data(const T1& d1, const T2* d2)//参数偏特化为引用类型时一定要初始化
    42. : _d1(d1)
    43. {
    44. cout << "Data" << endl;
    45. }
    46. private:
    47. const T1& _d1;
    48. T2* _d2;
    49. };

    注意:参数偏特化为引用类型时一定要初始化(使用初始化列表和缺省参数都可)

    偏特化有还有另一种表现方式: 当模版参数有多个时,部分特化一部分参数

    例如:

    1. // 类模板 -- 参数匹配
    2. template<class T1, class T2>
    3. class Data
    4. {
    5. public:
    6. Data() { cout << "Data" << endl; }
    7. private:
    8. T1 _d1;
    9. T2 _d2;
    10. };
    11. // 偏特化,不改变第一个模版参数,将第二个参数特化为int
    12. template <class T1>
    13. class Dataint>
    14. {
    15. public:
    16. Data() { cout << "Data" << endl; }
    17. private:
    18. T1 _d1;
    19. int _d2;
    20. };

    三、模板的分离编译

    我们现在使用声明和实现分离的编译的方法来实现一个函数模版(将类模版的声明放在Func.h头文件中,实现代码放在Func.cpp中):

    Func.h:

    1. #pragma once
    2. template<class T>
    3. T Add(const T& left, const T& right);

    Func.cpp:

    1. #include"Func.h"
    2. template<class T>
    3. T Add(const T& left, const T& right)
    4. {
    5. return left + right;
    6. }

    test.cpp:

    1. #include"Func.h"
    2. int main()
    3. {
    4. int x = 5, y = 10;
    5. Add<int>(x, y);
    6. return 0;
    7. }

    我们来编译一下:

    咦,怎么编译通过不了?还报了一个链接错误?

    下面我们来仔细分析一下:

    一份C/C++程序要运行,一般要经历一下步骤:预处理—-->编译--->汇编―—-->链接,不熟悉的同学可以看到这里:C语言程序环境_c语言程序运行环境

    但是在每个cpp文件经过预处理、编译、汇编,但没有进入链接阶段之前,都是单独被编译器所编译的。这样会导致由于模版参数没有实例化,Func.cpp文件在形成汇编指令时,并没有为Add函数开辟函数栈帧(因为没有参数的实例化,系统压根就不知道要开辟多大的空间来给Add函数用),但是由于头文件的展开,使汇编会形成call指令(这时的call指令并没有填上Add函数所在的栈帧地址),所以最终形成的obj文件进入链接时,会发现Func.cpp形成的obj文件中并没有上Add函数所在的栈帧地址,此时call指令中没有实际地址导致指令无法正常执行,最后报错

    3.1 解决方法

    知道了上面的原理后,我们就可以得出相应的解决办法了:

    1. 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。

    2. 模板定义的位置显式实例化。但我们不能给每个类型参数都实例化一份代码吧,这样要模版参数还有什么意义呢?所以这种方法不实用,不推荐使用

  • 相关阅读:
    VUE3学习 第三章 认识 ref全家桶、Reactive全家桶、to系列全家桶、computed计算属性、watch侦听器、watchEffect高级侦听器
    【随想】每日两题Day.8
    C++学习:list
    契约测试(中):利用PACT做契约测试
    在 Spring Boot 中使用 Dataway 配置数据查询接口
    SQL练习
    计算机网络 | 第三章 数据链路层 | 王道考研自用笔记
    Transformer - Attention Is All You Need - 跟李沐学AI
    关于tcp发送成功但对端无法接收情况的思考
    纯技术程序员的悲哀和出路
  • 原文地址:https://blog.csdn.net/m0_70811813/article/details/134088255