模版进阶及分离编译问题
在学习了模版基础 掌握了函数模板与类模板基础并手动实现了stl中一些容器的底层之后 我们已见识到模版对于泛型编程的强大之处 接下来再学习一些模版的其他知识
非类型模板参数
模板参数分 类型形参与非类型形参
类型形参 出现在模板参数列表中,跟在class或者typename之类的参数类型名称。就是之前实现的函数模版和类模版
非类型形参 就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常 量来使用。
如下面的第一行 对于之前学过的模版都是在class或者typename后面加一个参数类型名称来作为模版 在创建对象时候通过在<>里面写类型 来将模版实例化为真正的类或者函数
除了这样之外 还可以像下面第二行那样直接用常量的方式来创建一个模版参数 使用方式和之前一样都是在<>里面传参数来替换里面的值而实例化出新的函数或者类
但是这里的类型不是什么类型都可以的 在c++20之前只有整数类型的才可以 如int double size_t bool 类型 double不可以 在c++20里面double才可以支持了
这种方式就像c++之前的宏 如define N 10 这样的N是确定的只能是10 而这里新的方式可以在创建对象时候手动指定
和之前一样 这里的arr在没有创建对象的时候 模版不会被实例化只是模版 在下面实例化创建对象两个对象a1和a2 就会实例化出了两个不同的类
其实上面这样的类型就是stl中的array 接下来顺便了解array
它的存在是为了替换c中静态数组
c语言中静态数组的越界访问检查不严格 如下创建了一个十个int大小空间 但是以读的方式越界访问打印第十一个十二个位置的时候只会报警告 并直接不会报错
而以如下写的方式越界访问的时候就会报错
当然对静态数组写 采取的是抽查方式 不是所有的都会检查到
越界写抽查指的是编译器或运行时环境不会默认对所有数组或容器的越界写入操作进行检查,而是通过特定机制(如调试工具、手动检查或部分编译选项)抽样检测此类错误(有些就只是检查数组空间后面两个位置是否被越界访问写了 后两个位置访问写会报错 但是再往后的越界访问写不好报错)
所以对于c中静态数组的问题 越界读不检查,越界写只抽查 检查不严格
而stl中的array不管是越界读还是越界写只要发生越界访问的情况都会直接报错
但是在c++stl里有vector 开空间可以直接用vector 所有其实array很一般 它相较于vector的优点就是 vector是通过new在堆区来动态开空间的 而array是在栈区直接一下子把空间给开好的 速度要比vector快
模板的特化
概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些 错误的结果,需要特殊处理
拿下面的代码举例 函数Less实现两个值的比较 对于内置类型和重载了<的自定义类型都可以支持比较并且结果正确
但是用类型的地址进行比较虽然可以正常进行 但是我们预期是地址指向的内容比较的结果 而这个地址是不确定的 所以最终得到的结果可能不是我们所预期的
可以看到 对于这个函数模版 大多数情况是符合要求的 但是当是指针比较的时候我们期望的是拿指针指向的对象进行比较 但是里面实际是以指针的大小进行了比较 得到的结果就不一定是我们预期的了 所以就有了模版的特化 模版的特化就是为了针对这种特殊的情况进行处理
函数模板特化
先说一下 函数模版的特化不建议使用 因为针对下面第四点结合cosnt&实现起来会有一些麻烦 可以直接用函数的方式来替代它
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同
如下针对上面日期类类型指针Date*比较问题而特化出了一个专门针对日期类 类型指针的模版
这样如果到时候该模版里面的参数类型是日期类指针的时候就会调用这个特化出来的函数 而对于其他类型的也不会有任何的影响
但是对于函数模版来说 针对特殊的情况专门特化出一个函数 还不如用下面的方式直接重写一个这样的函数
对于使用的前三点没什么问题就是语法的规则 但是对于第四点使用时候可能出一些问题
如下面这样的情况 对于函数模版Less 如果形参是内置类型拷贝消耗代价小 但是如果是自定义类型的话进行拷贝的代价比较大 所以对于形参需要用&及const修饰
那么特化出来的函数根据第四点形参类型要和之前的模版匹配 对于它形参也应该用const修饰 但是像下面这样写就报错了
这是因为函数模版Less里面的const修饰的是left left不能不能改变 而对于特化的函数 这里cosnt在*的左边 意思是说left指向的对象不能改变 类型没有匹配上
如下 把const加在*的右边才代表是left不能改变 才和之前的模版相符 才会正常
其实对于特化出来的这个处理指针类的函数其实没必要用const及& 指针拷贝不会消耗多少 但是为了和之前的模版类型匹配 所以做了这样的处理而且cosnt修饰问题对指针很麻烦
而对于函数实现就没有这个顾虑了 所以对于函数模版特化其实没有必要使用 直接用一个专门处理的函数就可以了
类模板特化
类模版的特化分为全特化和偏特化(部分特化)
全特化
全特化即是将模板参数列表中所有的参数都确定化。
拿以下的类模版来举例
如下 这里全特化就把两个模版类型T1和T2都分别特化为了int和double 使用的方式和函数模版的特化类似
如下 当创建对象的时候 如果有和特化出来的类型匹配的会优先调用特化的类 否则匹配正常的类模版
偏特化
还是拿上面的类模版举例
这里只将第二个模版参数特化为double
在开始的类模版的基础上再特化两个类出来之后 用以下的方式来创建对象
上面的结果显示 如果传的两个类型都和全特化出来的类匹配的话 那么就会优先匹配这个全特化的类 如果有一个匹配一个不匹配会调用偏特化的 如果特化的里面都没有才会用模版实例化出一个新的类
所以在创建对象的时候会优先找全特化里面的有没有匹配的 没有再看偏特化里面有没有匹配的 都没有的话才会用模版来实例化对象
偏特化除了上面这样直接替换模版中的某个参数来特化之外 还有下面这种特化的方式做进一步限制
如上图在定义类名后面加了<>中加了模版类型的指针 这样代表如果到时候传的是任意类型的指针的话 就会进入到这个特化出的里面
如下面 对于第一个调用的就是基础的模版 只有是两个参数都是指针才会匹配这个偏特化出来的专门处理指针的 (这和之前的偏特化有区别 之前的偏特化有一个参数匹配就会调用偏特化的)
这种特殊的偏特化除了可以专门处理指针之外 还可以用对引用这样
也可以只用一个模版类型T1 然后<>里面是T1的指针和T1的引用
如下 偏特化出一个第一个参数是类型的指针 第二个参数是类型的引用 在创建对象的时候第一个参数传类型的指针 第二个参数传类型的引用就会调用这个专门特化出来的
如下 创建对象时候显示实例化传的是int* int& 那么在偏特化的类里面 形参T1和T2是什么类型呢
如下 可以看到里面的T1和T2都是int类型 而且如果T1是指针或者T2是引用那么在创建的时候必须初始化否则就会就会报错
所以虽然显示传的是int指针和int引用类型 在类里面T1和T2都是int类型 而如果传的是二级指针 那么T1就是一级指针
做这样的处理是为了 如果要在这个特化的类里面一些函数要做一些处理 如果这里T是指针使用起来就会受限 所以这样的设计方式使得我们更容易控制代码
那么这样的偏特化方式有什么用呢
如下 上一篇的最后 如果要处理日期类类型的指针 需要在类里面再专门写一个处理日期类的函数 那么如果再有其他类型的指针 每一个都由我们自己去写吗 所以这时候模版这种偏特化的方式就可以解决这个问题
class DateLess
{
public:bool operator()(Date* p1, Date* p2){return *p1 < *p2;}
};
如下 可以用这种特化的方式专门处理指针类型 只需要下面这一个特化的 就可以对所有类型的指针处理
模板分离编译
最开始学模版就告诉我们模版的声明和定义不要分离到两个文件中 那么为什么要这样呢
我们创建了三个文件fun.h fun.cpp test.cpp
在fun.h中用模版写了一个函数模版Add的声明 一个普通函数fun的声明 然后在fun.cpp文件中定义实现了这两个函数
函数模版Add定义和声明就分离了 在test.cpp中调用这两个函数 发现函数调用函数模版ADD报错了 是链接的错误
//fun.htemplate<class T>
T Add(const T& left, const T& right);int fun(int a,int b);
//fun.cpp# include "fun.h"template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int fun(int a, int b)
{return a + b;
}
//test.cpp# include "fun.h"int main()
{fun(1, 2);Add(1, 2);
}
其实各个文件会经历下面的过程
头文件不会参与编译 各文件在链接之前不会联系
fun.cpp和test.cpp都包含着fun.h 在预处理时候.h在两个cpp中展开 在test.cpp处理时候发现 fun函数和A得到函数都在这里声明了且在定义的地方类型参数个数类型都可以正确的匹配 都正常通过了编译阶段
在fun.cpp里面fun函数各个类型都是确定的 但是对于Add函数 T还没有确定没有实例化 在链接阶段 ADD找不到对应实例化的函数地址 无法正确的链接
解决方法
一种是在fun.cpp模版定义的地方实例出一个需要类型的函数 但这种不推荐使用
一种就是正常的让声明和定义放到一个文件当中 这样就不会出现链接的错误
模板总结
优点
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
缺点
1. 模版没有被实例化之前检查不严格 编译错误通常出现在模板被使用的地方 不容易发现出错的地方
2.模板会导致代码膨胀问题,也会导致编译时间变长
3.可读性降低 增加了理解难度
虽然模版有着一定的缺点 但是模版毫无疑问是一个非常伟大的发明 有着强大的泛型编程能力