【C++】模板进阶(非类型模板,模板特化,分离编译)

目录

 前言

一,非类型模板参数

1,前言 

2.场景

二. 构建模板特化

 (1.函数模板特化

(2. 类模板特化

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

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

(1)部分特化

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

三,模板分离编译

1.现象 

2. 解决方案

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

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

结语


 前言

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

一,非类型模板参数

1,前文

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

     template < class T>
    7 class arry{
    8   private:
    9   public:
   10     T net;
   11 };

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

   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
      }     

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

    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.场景

通常情况下, 使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 ,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。
// 函数模板 -- 参数匹配
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.函数模板特化

// 函数模板 -- 参数匹配
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.全特化 (相当于在一群中,服务一个人)

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

// 类模板
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)部分特化

将模板参数类表中的一部分参数特化。
// 类模板全特化
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)参数更进一步的限制 

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

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

//两个参数偏特化为指针类型
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.现象 

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

// 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.显示实例化(不推荐——治标不治本)

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

// 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.定义与声明头文件展开时在同一文件(最有效)

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

结语

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

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 36
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值