在本次的博客当中我们将会学习关于模板的其他的新奇的用法以及注意事项。
(一)typename和class的区别
在之前模板的学习当中,我们说到了对于模板当中的类型我们可以定义为class,或者typename却没有说明这两者有什么区别。实际上这两者几乎是相同的,我们一般使用class进行定义模板即可。但是在某些小地方来说,这两者也有一些区别。
例如:当我们想要编写一个类专门用于各种数据类型数据的打印的时候,我们就会发现系统当中出现了一个很奇怪的报错:
我们会发现错误提示当中告诉我们从属名称的使用必须以typename为前缀。并且第二个报错更为奇怪,他告诉我们要在16行多加一个;但是我们按照正常的语法来说已经加过;了呀?
我们具体分析一下原因:
我们这个使用的模板为一个适配器容器,我们可以通过适配器确定好想要输出的容器的类型。之后我们想通过该容器当中的迭代器进行遍历容器当中的数据内容,一切显示都很正常,其实不然。
回想一下我们底层逻辑的实现过程:iterator是我们自己创建出来的呀,对于我们创建的iterator来说这个是一个类型。可能为int*或者其他的什么。但是如果我们在类当中将其不设置为一个类型呢?我们不是还可以在类的内部创建一个内部类吗?
这个时候我们就会发现错误:因为我们并不知道iterator到底是什么东西。它可能是一个数据类型,也可能是一个内部类。所以编译器判断的时候就会产生歧义,进而产生这样的报错。
对于上面的报错我们的编译器觉得这个应该是一个数据的类型,所以就提示我们应该显示的表明typename防止产生歧义。下面的报错原因是我们的编译器觉得我们设计的语句应该是一个类,那么就应该是两条语句,两条语句之间应该使用;进行隔开。这就是我们报错的原因,因此我们只需要根据提示在语句的前面添加上typename显示的指出我们想要的是一个类型即可。
修改过后程序运行一切正常。
总结:
我们之前没有产生类似的报错的原因是:我们之前仅仅是使用该模板参数创建了一个对象,对于该对象我们仅仅是调用了其中的成员函数,成员函数的使用方式和语法跟我们成员变量的语法已经内部类的使用语法不会产生冲突,(成员变量的使用语法跟内部类的使用语法产生了冲突)所以就没有出现上述的报错情况。因此我们可以得出以下结论:
当我们对于模板实例化的类型当中使用域作用访问限定符::的时候我们需要明确指出我们想要得到的数据是一个内部类class还是typename,避免报错的产生。
(二)非类型模板参数
还有一种使用情况是我们很容易看到的:当我们想要创建一个定长数组的时候我们应该怎么进行初始化呢?
回想一下好久之前的知识:我们可以进行一个宏定义:
但是我们如果想要使用该变量进行不同长度的数据进行初始化呢?比如使用N对于A数组初始化为10,对于B初始化为100?
这个时候我们要是再使用宏定义就会显得比较繁琐,所以在C++当中引入了一个新的语法:非类型模板参数:我们可以通过模板的形式手动传入我们想要的定长数据的个数。
代码运行一切正常。
但是我们需要注意的是:对于非类型模板参数只能接受整形的数据,因为非类型的模板参数最大的作用就是控制数组的大小,对于数组的大小肯定没有半个对吧?
总结:
想要对类进行定长的控制,我们可以使用非类型模板参数。需要注意的是:非类型模板参数只能是整数类型的数据也就是说其类型只能是size_t或者int。
(三)函数模板的特化
还记得我们上一篇博客当中讲到的:仿函数吗?我们可以对于仿函数进行控制,使得我们得仿函数可以发挥类似于函数的作用。
但是如果如果我们不小心传成了指针呢?
我们会发现程序运行的结果出现了异常,我们期望的是可以输出12<15的结果。但是很明显我们的程序判断的是地址的比较结果。因此产生了错误。
那么我们可能会想:我们直接在类当中进行函数的重载操作不久行了吗?我们可以进行尝试:
我们会发现:根据输出结果我们的第二个对象并没有符合我们的预期进行第二个函数比较,而是被判断为整形类型的数据直接进入了第一个函数当中进行的比较。那我们应该怎么做才能使得我们的程序达到我们的预期呢?
这个时候就要用到我们的模板的特化的功能了。我们可以明确指出我们想要模板的特化类型的种类,编译器会优先进行匹配我们特化好的类。例如:
其格式就像重新编写了一个类,但是可以省略模板参数,我们需要在类名的后面指定我们想要匹配的数据的类型。运行之后会发现我们的程序恢复正常。
全特化与偏特化
向上述的内容进行的就是全特化,因为我们的所有的模板参数都被我们省略之后特化成为了具体的类型了,所以叫做全特化。
对于我们的模板不仅仅可以进行全特化还可以进行偏特化,顾名思义就是对指定的内容进行特化操作。举一个简单的例子:假如我们的模板参数有多个,我们就可以一部分不变一部分进行特定的赋值达到我们想要进行的效果。
对A类进行偏特化之后进行测试:
我们会发现代码函数匹配一切正常。
函数特化的功能:
函数特化的作用主要分为以下两种:
1.可以对类当中的参数进行特化操作,方便我们对于类实现相同运算符重载不同功能的作用。
2.也可以对我们的类进行特定的限制。
根据第二个功能举一个简单的例子:
如上图所示:如果我们想要传入的参数都是指针类型的时候,我们的编译器就会优先匹配我们该部分进行的特化操作。
我们会发现模板参数的个数都没有进行改变只是被限定成为了指针类型而已。代码运行一切正常:
总结:
模板的特化分为全特化跟偏特化,我们可以根据我们的需要进行指定的特化操作,便于我们对于特定的情况执行相应的内容。或者我们可以使用模板特化对我们原本的模板参数进行相应的限定。
(四)模板声明跟定义分离
回想一下:我们好像使用模板编写相应的代码的时候从来没有过将声明跟定义进行分离,但是模板究竟可不可以将声明跟定义进行分离呢?当然可以,但是有很多需要注意的事项,我们来对其一一进行说明:
我们先进行一个简单的声明跟定义分离的操作:
在不同的文件当中我们进行声明和定义的分离操作。但是在运行的时候我们会发现会产生异常的报错信息如下图所示:
我们显示了一个无法解析外部符号,这是为什么呢?仔细想一想声明跟定义分离的原理我们会想到,在编译器编译的时候会生成一张符号表。我们的函数声明和定义分离的查找就是靠这张表进行的。在表中会将函数具体的存储位置放到表当中。我们此处显示找不到位置说明我们的程序并没有实例化该函数。这又是为什么呢?
仔细想一想很容易想到——模板!因为我们的函数是一个模板函数,对于模板函数来说,在使用之前我们是不会进行实例化的。也就是说我们只有传入特定的类型的时候才会有相应的类生成,在编译完成之前我们就需要完成符号表地址的映射,但是又没有实例化相应的代码,所以就产生了报错。
既然知道了原因,那么解决方案呢?在这里其实有两种解决的办法。首先我们可以通过显示实例化的方式进行使用。也就是说提前告诉编译器我们想要实例化的类型让他提前完成,这样就可以找到相应的地址并链接了。
所对应的代码如下:
template为固定的格式,下一行跟上我们想要显示实例化的类型即可。再次运行代码:
代码运行恢复正常。但是我们如果想要生成其他类型的栈呢?这个时候就需要我们进行添加显示实例化的代码。很容易发现:由于我们的数据类型的种类有很多,所以如果采用这种方式解决的话会很繁琐。所以我们比较推荐下一种方式——将函数的声明和定义的分离放到同一个文件当中。
也就是说在我们的类当中存放函数的声明,在类的外面编写函数的定义。由于是一个文件这样就可以减少我们链接的步骤,就不会产生报错了。修改代码如下:
运行代码我们会发现:采用这种方式可以使我们的代码直接正常运行,无需添加显示实例化对象。