【C++】模板特化、模板分离编译

模板特化与分离编译

  • 一、非类型模板参数
    • 1.1 STL中的 array
  • 二、模板的特化
    • 2.1 概念
    • 2.2 函数模板特化
    • 2.3 类模板特化
      • 2.3.1 全特化
      • 2.3.2 偏特化
  • 三、模板分离编译
    • 3.1 什么是分离编译
    • 3.2 模板的分离编译
  • 四、模板总结

一、非类型模板参数

模板参数分类类型形参与非类型形参
类型形参即:出现再模板参数列表中,跟在 class 或者 typename 之类的参数类型名称
非类型形参:就是用一个常量作为类(函数)模板的一个参数,再类(函数)模板中可将该参数当成常量来使用。

当我们想定义以一个模板数组,但是不确定其长度时,我们只能以这种方式进行定义:
【C++】模板特化、模板分离编译_第1张图片

此时便暴露出其问题,关于数组的长度设定,我们没有办法设置其符合泛型编程的特点。

所以我们引入非类型模板参数的写法,就可以解决该问题。如下:
【C++】模板特化、模板分离编译_第2张图片
两者的区别:
【C++】模板特化、模板分离编译_第3张图片

这样,关于该成员变量数组的长度就可以由用户主动设置长度了。

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

1.1 STL中的 array

以上我们的举例,其实在 STL 中便有其原型 : array的介绍,其实原理就是按照我们上面的非类型模板参数来实现的。
【C++】模板特化、模板分离编译_第4张图片
其本质就是一个容器数组。
使用区别:
【C++】模板特化、模板分离编译_第5张图片
可能大家感觉这定义这么相似,两者间没有太大任何区别。

array本质上是一个容器,是使用对象定义的。
而 数组本质就是指针

既然 C++11 带来了 array ,就说明其必然是有优势所在。
优势在于:

array中,不管越界写、越界读都会被检查到。其对越界的检查非常严格
本质上,array 对越界的检查是通过函数的调用来检查,而数组本质上是指针解引用,报错、奔溃与否与编译器有很大关系。编译器检查越界是采用抽查的方式,而且只针对越界写入,越界读不检查。

二、模板的特化

2.1 概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数。
之前我们实现了一个日期类,那现在出现了以下场景:
【C++】模板特化、模板分离编译_第6张图片
大家发现,为什么第三个结果是错误的呢?
因为我们传入的是指针,所以 Greater 函数中是按两个指针的地址来比较的。
这肯定不是我们想要的结果。我们想即使我们传入日期对象的地址,调用该函数,内部使用指针指向的对象进行比较。
所以,这里便引入模板特化的技术。
**特化———针对某些类型进行特殊化处理**

2.2 函数模板特化

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

接下来我们将 Greater 进行特化(注意写法!),然后我们来观察结果。
【C++】模板特化、模板分离编译_第7张图片

注意
一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是讲该函数直接给出。

bool Greater(Date* left,Date* right)
{
	return *left< *right ;
} 

这种实现简单明了,代码可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板在此种场景下不适合特化。

2.3 类模板特化

我们使用仿函数代替上面的 Greater 函数,同样会出现以下这样情况:
【C++】模板特化、模板分离编译_第8张图片

所以,接下来我们对上面的仿函数进行特化,大家注意特化的格式。

【C++】模板特化、模板分离编译_第9张图片
此时可能会有人觉得特化很鸡肋,这里举一个很常见的使用场景。
比如之前实现的 priority_queue (优先级队列),实现了特化,我们可以直接将指针传入存放到优先级队列中,这样,队列中即是有序数据,而且提高了入队列、调整节点的效率。
【C++】模板特化、模板分离编译_第10张图片

2.3.1 全特化

其实特化分为全特化和偏(半)特化,具体是什么样的,以及其使用场景,我们一起往下学习。

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

全特化定义方式:
【C++】模板特化、模板分离编译_第11张图片

2.3.2 偏特化

任何针对模板参数进一步进行条件限制设计的特化版本。比如对于以下模板。

偏特化有以下两种表现方式:

  • 部分特化

一个参数可以是任意类型,而第二个参数类型必须为 char 型。

【C++】模板特化、模板分离编译_第12张图片

  • 参数更进一步的限制

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

【C++】模板特化、模板分离编译_第13张图片

接下来是引用偏特化,这个使用就比较奇特了。

【C++】模板特化、模板分离编译_第14张图片

当然,多变的偏特化还可以同时使用引用和指针

【C++】模板特化、模板分离编译_第15张图片
有了以上的基础,所以我们可以对我们上面的特化进行升级,直接使用偏特化,只要是指针,就调用该特化【C++】模板特化、模板分离编译_第16张图片
这样,可以进一步增强代码的通用性。

三、模板分离编译

3.1 什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译编译。

3.2 模板的分离编译

模板不支持分离编译

一般的 cpp 文件,test.h 中放入声明,test.cpp 放定义。

例如现在,我们将我们自己实现的 vector 容器中的 push_back 和 insert 放到另外一个文件中进行定义。

第一步就会碰到以下错误

【C++】模板特化、模板分离编译_第17张图片

为什么这个地方要加上 typename

【C++】模板特化、模板分离编译_第18张图片

原因:
因为面对这种场景,编译器无法区分用户这样书写,表示的是一个变量还是一个类型,所以需要我们人为去指定该场景下到底是变量还是类型,所以这里我们加上 typename 声明此时是类型。

接着我们运行我们的main()函数。

此时便出现了链接的错误。
而链接的错误提示为有声明push_back的声明,但找不到其定义。
【C++】模板特化、模板分离编译_第19张图片
为什么找不到 push_back 呢?

因为 push_back 没有被实例化.

【C++】模板特化、模板分离编译_第20张图片

总结
1.其余函数在编译期间就已实例化,确定了地址。
2.而push_back在vector.h外,在链接阶段进行匹配
3.push_back 由模板定义,由于模板参数还未实例化,则无法被编译为指令
4.所以push_back未进入符号表,无法被链接

解决方法:

1.模板的声明和定义不分离,或只分离到同一个文件中。
2.显示实例化,直接指定该模板参数的类型,然其进入符号表即可。

【C++】模板特化、模板分离编译_第21张图片
但是这种写法非常的鸡肋,如果此时 模板参数不为 int ,那这里又要添加一次显示实例化。
【C++】模板特化、模板分离编译_第22张图片
再添加一条显示实例化:
【C++】模板特化、模板分离编译_第23张图片
所以,这种方法并不推荐。
这里有一篇关于模板分离式编译更详细的讲解:
为什么C++编译器不能支持对模板的分离式编译

四、模板总结

优点

  • 增强复用了代码,节省资源,更快的迭代开发,将重复的工作交给了编译器
  • C++的标准模板库(STL)因此而生
  • 增强了代码的灵活性

缺陷

  • 模板会导致代码膨胀问题,也会导致编译时间变长
  • 出现模板编译错误时,错误信息非常凌乱,不易定位错误

你可能感兴趣的:(C++,c++,数据结构,开发语言)