【C++】模板进阶

目录

一,非类型模板参数

1,前言

2.场景

[二. 构建模板特化](#二. 构建模板特化)

(1.函数模板特化

[(2. 类模板特化](#(2. 类模板特化)

[1.全特化 (相当于在一群中,服务一个人)](#1.全特化 (相当于在一群中,服务一个人))

2.偏特化(相当于服务一类人)

(1)部分特化

(2)参数更进一步的限制

三,模板分离编译

1.现象

[2. 解决方案](#2. 解决方案)

1.显示实例化(不推荐------治标不治本)

2.定义与声明头文件展开时在同一文件(最有效)


前言

本文是模板初阶文章,建议先学习下文,更有利于理解
【C++】是内存管理,但C++ !! && 模板初阶_花果山~~程序猿的博客-CSDN博客

一,非类型模板参数

1,前言

我们回顾我们之前所使用的模板场景,大部分场景是作为类型模板。

cpp 复制代码
     template < class T>
    7 class arry{
    8   private:
    9   public:
   10     T net;
   11 };

但我们是否能回想起曾经的场景:

cpp 复制代码
   21 typedef N1 10;                                              
E> 22 typedef N2 10;
E> 23 typedef N3 10;
   24 
E> 25 int main()
   26 {
E> 27   int n1[N1];
E> 28   int n2[N2];
E> 29   int n3[N3];                            
   30   return 0
      }     

这个例子不是很贴切。需要定义多个宏,才能达到这样的效果,比较繁琐,而非模板参数可以解决这个问题

cpp 复制代码
    6 template < class T, size_t N = 20>                  
    7 class arry{
    8   private:
    9   public:
   10     T net[N];
   11 };
   12 
   13 int main()
   14 {
W> 15   arry<int, 10> n1;
W> 16   arry<int, 10> n2;
   17   return 0;
   18 }

可见,非模板参数是可以设置缺省参数,但值得注意的是参数类型一定得是整型(布尔类型也可以,float,double不兼容),且N不允许修改。

总之:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的
  2. 非类型的模板参数必须在编译期就能确认结果

补充:STL中的array接口代替原有数组,确实有全面检查越界好处,但不如直接用vector来的实在。

2.场景

通常情况下, 使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 ,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。

cpp 复制代码
// 函数模板 -- 参数匹配
template<class 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;
}

可见p1, p2是指针,而这种类型无法使用此模板,因此我们需要进行模板特化。

二. 构建模板特化

即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式 。模板特化中分为函数模板特化类模板特化。
函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表必须要和模板函数的 基础参数类型 完全相同,如果不同编译器可能会报一些奇怪的错误

(1.函数模板特化

cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
 return left < right;
}

// 全特化
// 对Less函数模板参数类型进行指定一个唯一类型
template<>
bool Less<Date*>(Date* left, Date* right)
{
 return *left < *right;
}

// 偏特化
template<>
bool Less<T*>

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;
}

上面的例子,就是为参数是指针对象进行比较。普通的模板无法比较,只能再写一个重载函数。

(2. 类模板特化

1.全特化 (相当于在一群中,服务一个人)

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

cpp 复制代码
// 类模板
template<class T1, class T2>
class Data
{
public:
 Data() {cout<<"Data<T1, T2>" <<endl;}
private:

 T1 _d1;
 T2 _d2;
};
// 类模板全特化
template<>
class Data<int, char>
{
public:
 Data() {cout<<"Data<int, char>" <<endl;}
private:

 int _d1;
 char _d2;
};

2.偏特化(相当于服务一类人)

(1)部分特化

将模板参数类表中的一部分参数特化。

cpp 复制代码
// 类模板全特化
template<>
class Data<int, char>
{
public:
 Data() {cout<<"Data<int, char>" <<endl;}
private:

 int _d1;
 char _d2;
};

// 类模板偏特化
template<>
class Data<T, char>
{
public:
 Data() {cout<<"Data<int, char>" <<endl;}
private:

 T _d1;
 char _d2;
};

(2)参数更进一步的限制

偏特化并不仅仅是指特化部分参数,而是针对模板参数 更进一步的条件限制所设计出来的一个特化版本。

比如:指针类型,引用类型比较特别

cpp 复制代码
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{ 
public:
 Data() {cout<<"Data<T1*, 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)
 {
 cout<<"Data<T1&, T2&>" <<endl;
 }
 
private:
 const T1 & _d1;
 const T2 & _d2; 
 };
void test2 () 
{
 Data<double , int> d1; // 调用特化的int版本
 Data<int , double> d2; // 调用基础的模板 
 Data<int *, int*> d3; // 调用特化的指针版本
 Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

注意:当我们拷贝到编译器中时,发现我们运行不了这段代码,原因是什么呢?

原因:因为类模板的全特化,偏特化不是全新的模板,是需要存在基于原来的类模板为基础的代码,才能语法通过。

从结果来看,就像厨师做菜一样,为了保证出菜效率,我们会选择成品,半成品,原材料。全特化就是成品,偏特化则是半成品,原材料则是基础模

板。

三,模板分离编译

1.现象

测试一下场景,函数模板声明,定义分离在两个文件中的场景。

cpp 复制代码
// a.h
template<class T>
T Add(const T& left, const T& right);
func();

// a.cpp
#inlclue "a.h"
template<class T>
T Add(const T& left, const T& right)
{
 return left + right;
}

void func()
{
   cout << "func" << endl;
}

// main.cpp
#include"a.h"
int main()
{
 Add(1, 2);
 func();
 return 0;
}

结果:你会发现,编译不通过。而我们注释掉main函数中的Add调用后,就能编译通过。

分析:

我们知道代码编译过程有,预处理(头文件展开,条件编译,剔除注释) ------> 编译(进行语法检查,生成语法树,生成汇编代码(给人看的)) ------> 汇编(生成机器码,注意:头文件不参与编译)------> 链接(根据修饰后的字符名,补充跳转地址)
其实是问题出在链接,文件之间在前3个过程,相互独立编译。编译时,普通函数可以生成地址,而函数模板因没有实例化所以无法生成地址,在链接时期,无法解决地址问题,所以无法编译成功。

啥?你问为啥函数模板不像普通函数一样,去其他文件中寻找定义并且实例化呢?

答:

先回答普通函数,普通函数的地址就是其函数定义代码的第一行地址,在链接时在函数声明处被填充,CPU可以快速找到。

而函数模板,因为链接时没有留下地址(因为T未实例化),得靠修饰后的函数名在大量的文件中寻找,本身这是一个很浪费资源的操作,所以编译器索性懒得寻找模板函数定义的位置,只允许函数模板定义与声明在同一个文件中。(方便查找到模板函数,并且实例化)

2. 解决方案

1.显示实例化(不推荐------治标不治本)

在模板函数定义位置,进行显示实例化。

cpp 复制代码
// a.cpp
#inlclue "a.h"
template<class T>
T Add(const T& left, const T& right)
{
 return left + right;
}

void func(){cout << "func" << endl;}

template //2参数是小数
double Add<double>(const double& left, const double& right);

template // 2参数是整型
int Add<int>(const int& left, const int& right);

// main.cpp
#include"a.h"
int main()
{
 Add(1, 2);
 func();
 return 0;
}

缺陷是被动添加,每当出现一个新参数就需要添加,丧失模板的灵活性。

2.定义与声明头文件展开时在同一文件(最有效)

如小标题一样,定义与声明在头文件展开后要在同一文件中。

结语

本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

相关推荐
娅娅梨2 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控7 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
汤米粥8 分钟前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾11 分钟前
EasyExcel使用
java·开发语言·excel
我爱工作&工作love我14 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
拾荒的小海螺17 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)42 分钟前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
娃娃丢没有坏心思44 分钟前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h44 分钟前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20