目录
前言:
本篇来介绍模版的更复杂更多样化的一些用法,方便后面的学习与使用,模版的初阶见:模版初阶。
1.非类型模版参数
//非类型模版参数
//template<class T,size_t N>
//class Array
//{
//public:
// Array()
// {}
//private:
// T _a[N];
//};
//
//template<class T,bool flag=false>
//void func(const T a)
//{
// int b = 0;
// if (flag)
// b = a;
// cout << b << endl;
//}
非类型模版参数就是常量。
如代码也可以是bool值等等,好处就是相比于#define定义的宏,之后就不能修改了,而非类型模版参数相比灵活一些:
//int main()
//{
// Array<int, 10> a1;
// Array<double, 20> a2;
//
// func<int,true>(2);
//
// return 0;
//}
Array类传的第二个模版参数是多少,类中的N就是多少。
func函数第二个模版参数可以传布尔值(当然可以是0和1),传的是多少,func函数中flag就是多少 ,如果不穿,就用默认的false。
那非类型模版参数有什么用呢?在数据结构 位图的实现中会使用。
2.函数模版的(全)特化
首先,需要我们注意,不论是函数模版的特化还是下面的类模版的特化,都必须基于一个父模版。
例如:
我们要来比较下面的类型,就可提供一个函数,并借助函数模版:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main(){
cout << Less(1, 2) << endl;//结果1
Date d1(2024,5,13);
Date d2(2023,5,13);
cout << Less(d1, d2) << endl;//结果0
return 0;
}
注意d1与d2比较时,会调用Date类中的operator<()进行比较。
但是下面的情况该怎么解决呢?能直接比较吗?
template<class T>
bool Less(T left, T right)
{
return left < right;
}
Date d1(2024,5,13);
Date d2(2023,5,13);
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;//不特化,结果比较的就是指针指向空间地址的大小,结果一直为真
如果直接调用普通的函数模版,T被实例化为指针,那比较的就是指针了,指针比较比较的是什么?是指针指向空间的地址的大小,如果p2的地址大小大于p1的,那比较的结果会永远为真。
所以我们使用函数模版的方式怎么解决?使用函数模版的特化:
//函数模版的特化---针对Date*类型的全特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;//指针比较大小比较的是指向空间地址的大小,但是解引用就是比较的Date类型,就会调用Date的operator<
}
当T被实例化为Date*时,就会去调用模版的特化,所以比较p1p2就会先解引用,找到指针指向的内容也就是d1与d2,然后再比较就会调用Date的operator<()了。
其实我们一般使用函数的重载来解决这样的问题,可读性更高,并且代码简洁明了:
//但是函数模版的特化没有直接函数重载可读性高,简单明了
//bool less(Date* left, Date* right)
//{
// return *left < *right;
//}
如果函数重载与函数模版特化都存在,那会调用函数重载,因为函数在编译阶段就call到地址了,而模版还需要实例化。
上面的特化称为全特化,模版还有偏特化,偏特化在类模版中说明,而函数模版是不支持偏特化的:
//函数模版不允许使用偏特化
//template<class T>
//bool Less<T*>(T* left, T* right)
//{
// return *left < *right;
//}
在数据结构篇哈希表的实现中会使用。
3.类模版的特化
正常我们来进行上面的比较都会提供一个仿函数,而仿函数是封装在一个类里面的:
template<class T>
struct Greater
{
bool operator()(const T x, const T y)
{
return x > y;
}
};
int main(){
cout<<Greater<int>()(1, 2)<<endl;//结果0
Date d3(2025, 5, 13);
Date d4(2026, 5, 13);
cout << Greater<Date>()(d3, d4) << endl;//结果0
return 0;
}
但是也是会碰到上面对于指针的比较,如果直接使用普通类模版,比较的也是指针指向空间地址的大小。所以提供一个类模版的全特化。
全特化:
//类模版的全特化---针对Date*的全特化
template<>
struct Greater<Date*>
{
bool operator()(const Date* x, const Date* y)
{
return *x > *y;//解引用后调用Date的operator>;如果不特化,指针的比较就按指向空间的地址比了
}
};
这样先解引用,找到指针指向的内容,也就是d3d4对象,再比较会调用Date的operator<()。
偏特化:
那如果传的类型是int*等等呢?需要我们一个一个写针对某个指针类型的全特化,这时,就可以使用偏特化来解决问题:
//针对指针的偏特化
template<class T>
struct Greater<T*>
{
bool operator()(const T* x, const T* y)
{
return *x > *y;
}
};
int main(){
int* p5 = new int(1);
int* p6 = new int(2);
cout << Greater<int*>()(p5, p6) << endl;
return 0;
}
这样只要模版实例化为指针类型,就会调用指针的偏特化,引用也可以这样玩。
注意,如果指针的偏特化与Date*这样的全特化一起存在时,传模版参数是Date*会调用哪一个?
编译器当然是调用最匹配的那一个,也就是针对Date*的全特化,因为全特化的限制更多。
偏特化还可以用来进行部分特化:
template<class T1,class T2>
class Test
{
public:
Test()
{
cout << "Test<T1,T2>" << endl;
}
private:
T1 _t1;
T2 _t2;
};
//部分特化
template<class T1>
class Test<T1,int>
{
public:
Test()
{
cout << "Test<T1,int>" << endl;
}
private:
T1 _t1;
int _t2;
};
int main()
{
//调用部分特化
Test<int, int> t1;
//调用部分特化
Test<double, int> t2;
//调用普通的类模版(父模版)
Test<int, double> t3;
return 0;
}
4.模版的分离和编译
首先,我们先复习一下:
如果函数的声明与定义分离,那么程序运行,函数就会在链接的时候确定地址。
如果声明定义放到一起,那么在编译阶段就可以call到这个函数的地址了。
如上图也可以知道如果声明定义分离,在链接的时候才会合并文件,这样在链接时就能找到函数的实现,才能确定函数的地址。
模版声明定义分离的问题:
那模版的声明与定义分离也是这个问题,声明与定义分离会导致在声明文件中(也就是头文件中),生成了目标文件时模版还没有实例化,那就确定不了函数的地址,主文件生成的目标文件时模版也没有实例化,也确定不了函数的地址,链接时就会发生错误。也就是在生成了目标文件时还都没有实例化,这是编译器做出的处理,那能不能让编译器做到在编译阶段(也就是生成.s文件的时候)就进行实例化呢?不能,因为对于大型工程来说,会导致编译的效率变慢很多。
解决方法:
a.显示实例化
坏处就是需要什么类型就要实例化一份出来。
b.声明定义放到一个文件,前声明,后定义
声明与定义放到一起,这样模版直接就能实例化了(在什么时候实例化呢?在编译的时候实例化)编译的时候就能call到这个函数的地址了,不用再进行链接了。
那类模版声明定义分离有意义吗?
如果成员函数在类中定义,编译器可能会将它当做内联函数处理,所以在stl源码里面有些短小的函数直接就放在类里面定义了,就是当做了内联函数,有些大一点的函数就类中声明,类外定义,源码就这样搞的,所以声明定义分离还是有一定的道理的(什么道理?)。
另外,有些地方可能会定义头文件为.hpp,但实际也是头文件,只是暗示这是模版,声明定义写到一起了,下图为boost库:
总结:
膨胀就是指实例化出各种各样的模版参数,文件会变大,因为只要一实例化,就是一份新的代码,但是不可避免。
编译时间变长是因为模版实例化的时候是在编译阶段。
一般模版的错误从头开始看,就容易找到了。