1. 非类型模板参数
模板参数分为 :类型形参与非类型形参 。 我们之前更多的在使用的是模版类型参数类型形参:出现在模板参数列表中,跟在 class 或者 typename 之类的参数类型名称 。非类型形参,就是用一个常量作为类 ( 函数 ) 模板的一个参数,在类 ( 函数 ) 模板中可将该参数当成常量来使用 。
比如:
比如想在类中定义一个静态的栈:
C语言的作法我们会使用宏
(注意,宏后面是不需要加分号的)
#define N 100
template<typename T>
class stack {
int _a[N];
int _top;
};
int main() {
stack<int> st1;
return 0;
}
如果想要一个装10个,一个装100个,那就需要一个常量参数来记录我们的数值
//#define N 100
template<typename T,size_t N = 100>
class stack {
int _a[N];
int _top;
};
int main() {
stack<int> st1;//100
stack<int, 10> st2;//10
return 0;
}
但是st1和st2底层本质还是两个不同的栈: 他们分别是编译器生成的两个不同的栈。
一般情况下,只有整形会被用于定义非模版参数
不过在C++20之后就支持double和指针等内置类型作为非类型模版参数,但是依然不支持如string等非内置类型。
关于非类型模版参数的运用,
很多场景还是用于定义一个新的数组。
并且传进来的这个N是常量,是不会被修改的。
再看看c++标准库中非类型模版参数的运用:
C++11中加入了一个array类(用类封装的静态数组),其中就使用到了非类型模版参数
使用:
那么array有什么优势呢?
在C语言中,由int a[10]这样定义出的数组中,对于越界的检查是采取的:先设置标志位,随机抽查
并不是在一个数组越界之后的所有位置都能检查出来。比如对数组a,一般情况下,a[10]和a[11]都能被检查出来越界,但是a[12]不一定能被检查出来。
甚至,以上抽查都只能检查越界写;
越界读是检查不出来的,可以直接读出一个随机数。
那么arry为什么能做到检查是否越界呢?
arry作为自定义类型,可以对方括号访问进行自定义,只要在调用operator[]的时候注意检查数字即可(库中也的确是这样实现的)。这样既能检查读,也能检查写
不过array是一个不够完美的设计,vector完全可以作为其替代品。
还有一个以后会学习的容器:
bitset也需要使用非类型模版参数
2. typename的使用
当我们想要打印一个vector的时候:
如果传具体类型进来就没事(vector<int>)
但是传一个vector<T>就不行。
会报错,但是在vector<int>前面加一个typename即可:
而非:
编译器只会对模版的表层进行检查(比如有没有分号,括号是否个数正确)
并且只有到最后要调用、使用的时候才会去实例化。意思就是编译器现在需要在一个没有实例化的类,也就是vector<T>中去取一个const_iterator
也就是到一个新的内域里面去取一个东西出来,这个东西可能是 静态变量 也可能是 类
(因为::,编译器无法区分要访问什么内容 访问 静态变量和访问其中的类型 的语法是一样的
而非静态变量是用对象. 访问的)
类模版实例化前,编译器不会去查细节的东西,所以编译器不知道到底是静态成员变量还是类
加typename , 意义就是告诉编译器后面(也就是显式说明const_iterator是一个类)是一个类,等vector<T>实例化之后再去找这个类。
凡是在一个没有实例化的模版中取东西(常见场景就是取iterator), 都需要使用这个语法。
或者还有种方法可以避开所有的问题: 使用auto
小试身手:使用哪种Container不会报错?(list vector deque)
找错 :
1.首先,Container前面应该加一个typename ,表示取的东西是一个类(iterator是一个类)
2. vector、deque底层都是用了连续空间,所以虽然++iter迭代器了,但是erase(tempit)以后
底层是连续空间,删除会挪动数据,最终导致iter意义变了,已失效了。而库中的检查会直接报错
而list,不是连续空间,删除以后tempIt虽然失效了,但是不影响iter。
总结:在一个没有实例化的类域中取出一个类,需要用typename声明,告诉编译器这是一个类。
3. 模版的特化
3.1 函数模版特化(注意const T&特化时写成Date* const &)
通常情况下, 使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果 ,需要特殊处理。
比如写一个比较大小的函数模版
// 函数模板 -- 参数匹配
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;
}
指针的比较明显是错误的,需要对模版进行特殊化处理:
其他类型正常走模版,需要特殊化处理的类型就走下面的特殊模版。
但是当函数模版不止是用T,而是用const T&时
语法比较奇葩:
函数(类)模板的特化步骤:1. 必须要先有一个基础的函数(类)模板2. 关键字 template 后面接一对空的尖括号 <>3. 函数(类)名后跟一对尖括号,尖括号中指定需要特化的类型4. 函数形参表 : 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
为什么不写成const Date* & left呢?
如果这样写的话const修饰的其实是Date*而不是left
要特化的话,特化里的参数必须和模版里的一模一样,只有把const写在后面才是正确的
不过鉴于特化有这么多毛病,直接用函数去重载,写一个“最符合口味”的函数即可。
并且,当特化和重载的函数同时存在时,编译器会调用直接调用现成的函数 。
所以要谨慎使用函数模版特化。
3.2 类模版特化 (实用)
类模版特化相对实用很多(因为类是没有重载的做法的)。
比如我们想特殊处理传入int和char的情况:
在上文中,我们在处理比较Date*的时候,发现还需要自己再去写一个新的仿函数,否则无法完成指针的比较。
但是经过本文的学习,可以通过实现一个类模版的特化来完成。
直接将less写一个特化版本,而非PDateLess ,这样就能只传一个仿函数myless,让myless自己去适应版本。
以上两段代码作为仿函数整体被传给:
3.3 偏特化(半特化)
以上所讲都是全特化,还有偏特化。
(要将哪个作为参数传递,哪个就要在template后面写出来)
场景二:限定模版类型
只要传的是指针,就能匹配上,并且按照这个新的版本进行执行。
比如你传一个Data<int* , double*> , 那么T1就会被识别为int , double 就会被识别为T2
*则会自动与尖括号中的星号抵消掉。
Data* 、 int* 和 double*和所有的指针都可以通过这个偏特化解决,而不是特化一次解决Data*,再特化一次解决int*
除了限定指针场景,还可以限定引用场景,也可以混着搭配,一个引用一个指针。
(下图是两个引用)
4. 模版的分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。声明和定义分离可以有效避免冲突的问题。比如有一个容器(没有声明定义分离),A希望使用,B希望使用,C也希望使用,大家都包含进自己的容器中,但是在最后链接的时候就会因为重复定义而出错。
在分离编译时,只有声明不会有地址,地址来自于定义的第一个指令的地址。
但是如果将模版的声明和定义,就会在最后链接的时候找不到地址而报错。
复习一下链接:先将.h都包含到.cpp文件中,最后再将所有的.cpp文件变成.o(.obj)文件,最后再合并成一个可执行文件。
比如我们有三个文件 Func.h Func.cpp test.cpp
主函数在test中,而Func中含有两个我们希望执行的函数,一个是函数模版,一个是普通函数
调用Add的时候知道要怎么实例化, 但是有定义的地方不知道会实例化成什么,不去实例化就没有函数地址,函数地址没有被放到符号表
解决方法:直接将定义写在.h中。.h会被直接被包含进调用该函数的.cpp 直接有定义就不用再在链接的时候找了。官方库也是这么操作的,短的函数直接放里面,长的放外面。但是放外面的时候也要把类域和模版等问题控制清楚。