C++·模板进阶

发布于:2024-07-10 ⋅ 阅读:(199) ⋅ 点赞:(0)

1. 非类型模板参数

        之前我们写的模板参数都设定class类型的,这个模板参数用来给下面的代码中的某些元素定义类型,我们管这种模板参数叫类型形参。非类型模板参数就是用一个常量作为模板的一个参数,在模板中可将该参数当作常量来使用,这种模板参数叫非类型形参

                        ​​​​​​​

        比如我们想创建一个静态的数组的类,或者说固定大小的数组的类。如果是以前的思路就是将N定义成一个宏,但是如果那样的话无法生成多个不同大小的静态数组。此时就可以借助非类型形参,定义多个任意长度的静态数组。

        非类型形参跟类型形参一样也可以给到缺省值。

       在C++20之前,只允许整形做非类型模板参数,C++20之后可以支持double、指针等其他的内置类型

        在C++库中有专门的静态数组容器,原理用到非类型模板参数

        官网资料:array - C++ Reference

                                ​​​​​​​        

        这种静态数组的容器比C语法数组的优势在于更加严格的越界检查,在使用C语法的数组的时候它的越界检查是一种抽查原理,如果越界越的很远程序就检查不到,同时不修改越界内容也会检查不到越界行为,但是在array容器中只要越界就一定当场报错。

        但是array也不是什么好东西,因为vector完全可以替代它。

2. typename的使用

        我们上一次接触typename的时候是在定义模板参数类型的时候提到过的,当时说class和typename是通用的。这里先解释一下原因,typename可以理解成是一个类型名,但不完全是,它的作用就是告诉编译器我后面跟的这个东西是一个类型名,就是说typename是类型名的类型名。

        我们看下面两段代码

        左边的代码还没什么问题,但是到了右边,当我们把它写成模板想让这个函数可以打印所有类型的vector的时候就出现了报错。

        我们知道类或模板都有一个按需实例化的规则,就是说在预处理阶段,vector<T>::const_iterator 并不会被实例化,因此编译器无法确定这个名称是类型名,还是变量名,如果是变量名的话很明显这行代码的语法就不对。因此右边这段代码就是因为这个不确定是否是类型名的原因而报错的。

        为了解决这一问题,我们将要用到 typename 告诉编译器,我后面这个是个类型名,你不用怀疑这里语法的对错了。

        ​​​​​​​        ​​​​​​​        

        当然,还有一种更加便捷的解决办法:

        ​​​​​​​        ​​​​​​​        

        直接用 auto 代替那一大堆,因为auto一定是一个类型名,后面语句的返回值是什么类型,auto就会控制这个 it 变量是什么类型。

3. 模板的特化

3.1 概念

        通常说,使用模板可以实现一些与类型无关的代码,但对于一些特殊的类型,就可能会出现错误,此时就需要将模板特化进行处理

        ​​​​​​​        ​​​​​​​        ​​​​​​​

        比如上面这段代码日期类之间的比较可以通过调用其成员函数进行比对,但是当想通过两个日期对象的地址进行二者内容上的大小比较时就出现问题了,因为这样些less函数之间比较的是二者地址上的大小,而不是地址中内容的大小。

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

3.2 函数模板特化

        先有一个基础的函数模板,然后再写一个template,函数名后面跟一对尖括号,其中指定需要特化的类型。

        函数形参表必须要和模板函数的基础参数类型保持一致,否则会出现错误。

        ​​​​​​​        ​​​​​​​        

        这么写坑很多,函数形参的类型const的位置等问题很不好把控,所以推荐用直接写函数来代替函数模板,因为当同名函数和函数模板同时存在的时候,编译器会根据参数类型去匹配贴合度更高的一方。

        ​​​​​​​        ​​​​​​​          

        函数重载这种实现方法简单明了,代码可读性高,容易书写,因此函数模板不建议特化。

3.3 类模板特化

3.3.1 全特化

        全特化就是将模板参数列表中的参数全部都确定化

        ​​​​​​​        ​​​​​​​        

        可以看到,通过控制实例化时的模板参数类型,在实例化d2的时候调用的就是全特化模板,这中模板的特化操作很类似于函数的重载

3.3.2 偏特化

        针对模板参数的其中一部分进行特化,或者进行进一步的筛选控制

        ​​​​​​​        

        上面这段代码中只限制了第二个模板参数的类型,只要第二的模板参数的类型是int型,那么就会被特殊处理

        ​​​​​​​        

        这种特化方案的限制范围更加宽泛,只要两个模板参数都是指针那么就进行特殊化处理。

        在指定为指针特化的时候有一个值得注意点

        

        可以看到T1和T2的类型并不是指针。这个规则对于特化引用等场景时也通用。

4. 模板的分离编译

        我们之前提到过写模板的时候无论是类模板还是函数模板都要把声明和定义写到一个文件中去,下面就浅析一下原因。

        对于一个普通的函数func来说,声明定义分离后在别的文件中想调用这个func函数,那么就会在该文件中包含func函数所在的声明文件。此时这个别的文件就会拥有func函数的声明,通过这个声明可以在符号表中找到func函数的真实地址,从而调用这个函数。

        但是对于一个模板函数来说,它是模板,并不是函数,因此这个东西并没有真实的地址,所以在别的文件中包含了模板的声明,也无法对应到真实的函数地址,从而导致链接错误。

        不过这一问题有解决办法,就是在模板定义的位置显示实例化。

        ​​​​​​​        

        这样就相当于可以在编译阶段告诉编译器,我后面会将这个模板实例化成 int 和 double 因此就不会出现找不到对应函数而产生的链接错误了。

        当然,最推荐的还是直接把函数模板实现写在 .h 文件中,这样也避免了各种问题

        类模板的声明和定义也是都要写在一个文件中,但是因为类模板的成员函数可能比较长,因此可以把类模板中的成员函数实现在类模板外面,把成员函数的声明放在类模板里面,但是成员函数的定义也要与类模板保持在同一文件中。

        如果把成员函数直接写在类模板中,这个函数就成为了一个内联函数inline

        

5. 模板总结

        优点:

                1. 模板复用了代码,节省资源,可以更快的迭代开发,C++的标准模板库(STL)因此产生。

                2. 增强了代码的灵活性

        缺点:

                1. 模板会导致代码膨胀问题,也会导致编译时间边长

                2. 出现模板编译错误时,报错信息会非常混乱,不易定位错误

        但总的来说模板的弊端都是我们可以接受的,模板这个东西还是一个非常好的发明。