目录
关于C++模板的使用在《C++类和对象》中有介绍,本篇博客主要帮助读者对C++模板的一些特殊用法进行总结。
非类型模板参数
模板参数分为两种:类型模板参数和非类型模板参数。
类型模板参数:跟在class和typename后的类型。
非类型模板参数:用一个常量作为参数,即不再使用class或typename来定义的参数。
template<class T,size_t N=10>
class Seqlist
{
public:
private:
T _arr[N];
size_t size;
};
如上图所示:T是一个类型模板参数,用于替代各自类型;而N就是一个非类型模板参数,用于记录说要开辟的静态顺序表的大小,默认为10。
简而言之:就是有具体类型的参数就是非类型模板参数。
但是同样非类型模板参数也有一些要求:
1)参数应被当作常量使用。
2)参数类型必须是整形(bool,int,char,enum),指针,引用。(C++20添加了部分其他类型);
类模板的特化
在一些情况下,我们又是要对类的部分类型进行特殊化处理,即T在不同类型下,可能会出现不同的需求(函数实现),此时就需要引入类模板的特例化。
template<class T1,class T2>
class Date
{
public:
///
/// .....实现类成员函数
///
private:
T1 _a;
T2 _b;
};
上面代码是非特化模板,T1和T2可以表示任意类型。在编译阶段才去实例化为具体类型。
template<>
class Date<int, int>
{
public:
///
/// ......实现类成员函数
///
private:
int _a;
int _b;
};
以上代码是对Date类进行特例化,指明是<int ,int >类型。类模板特例化后相当于一个新的"类",但是这个类也不能脱离非特例化类独自出现,因为相当于新"类",所以需要自己再手动实现其功能(函数)。
那么对于特例化模板参数和非特例化模板参数来说,编译器会对非特例化模板参数进行实例化来实现类???
实际上是不会的:如果会那我们对模板特例化就没有了意义,同时编译器已经识别了类型匹配,它会偷懒直接去掉现成的,而不是去耗时耗力的实例化。
Date<int, int> d1; //去直接调用特例化的模板类
Date<char, int> d2; //将模板参数实例化后调用
分类
类模板的特化分为两种:全特化和偏特化。
都是字面意思:全特化就是全部模板参数都有指定;偏特化指的是部分模板参数被指定或者对某一类型进行限制。
//非特例化
template<class T1,class T2>
class test
{
public:
private:
T1 _a;
T2 _b;
};
全特例化:不用给模板参数
//全特例化
template<>
class test<int, char>
{
public:
private:
int _a;
char _b;
};
偏特例化:部分参数指定
//偏特例化
template<class T2>
class test<int,T2>
{
public:
private:
int _a;
T2 _b;
};
偏特例化:对类型进行限制;
此处演示特例化为指针,也可以限制为引用...
//偏特例化:对类型进行限制
template<class T1, class T2>
class test<T1*,T2*> //将其参数限制为指针
{
public:
private:
T1* _a;
T2* _b;
};
当然也可以对类型进行混合限制。
//偏特例化:对类型进行限制
template<class T1, class T2>
class test<T1&, T2*> //第一个参数限制为引用,第二个参数限制为指针
{
public:
private:
T1& _a;
T2* _b;
};
注意:以上代码的成员函数均没有写出,但是要知道的是:特化的类成员,类函数需要自己写来进行特殊化处理。
函数模板的特化
与类模板一样,函数模板也能进行特例化。
下面实现一个比较的函数。
template<class T>
bool Less(T x, T y)
{
return x < y;
}
以上是实现一个各种类型的交换函数。但是当实参是指针的时候,比较就会比较的是指针的地址,这不是我们想要的结果,所以进行特化。
template<>
bool Less<int*>(int* x, int* y)
{
return *x < *y;
}
template<class T>
bool Less(T* x, T* y)
{
return *x < *y;
}
模板分离编译
问题
模板分离编译是一个很不推荐的写法,不提倡将函数的声明和定义分离到两个文件中去。
以下代码实现一个仿函数来实现小于比较。
"test.h"头文件
namespace less
{
template<class T>
class Less
{
public:
bool operator()(T x,T y);
};
}
test.cpp文件
template<class T>
bool less::Less<T>::operator()(T x, T y)
{
return x < y;
}
main函数文件调用
using namespace less;
Less<int> less;
int a = 10;
int b = 33;
cout << less(a, b) << endl;
可以看到将类的函数的声明和定义分离,但是调用后却报错了。
可以看到此处运行时不是编译错误,而是连接错误,编译器没有找到指定的函数,为什么呢??
编译器没有找到函数意味着在test.cpp文件中函数的地址没有进入函数表。
原因在于:在编译期间,每个文件都是分开编译的,main函数中只包含头文件,检查语法时编译器在头文件中找到了仿函数的声明认为有这个函数,但是test.cpp文件中的仿函数是模板函数,相当于一个图纸,没有实例化所以不会在test,cpp函数表中生成函数地址,test.cpp文件函数表中也就没有仿函数地址,链接时就会报错了。
解决方法
1)不对模板定义进行分离或对模板进行特例化;
此处对上面代码中的模板进行特例化。
头文件显示实例化
template<class T>
class Less
{
public:
bool operator()(T x,T y);
};
template<>
class Less<int>
{
public:
bool operator()(int x, int y);
};
test.cpp文件
bool less::Less<int>::operator()(int x, int y)
{
return x < y;
}
2)将声明和定义放在同一个文件
在C++的库中,常常使用"xxxx.hpp"或"xxxx.h"文件来存放类模板函数的声明和定义。
补充
typename和class的区别
typename和class都是模板类型的声明,在大多数情况下其没有本质区别,但是typename有指明类型的意思。以下是只能用typename的特例。
template<class T,class Container=deque<T>>
class Print
{
public:
void test()
{
Container::const_iterator it = _con.begin();
}
private:
Container _con;
};
以上代码,类成员函数test中定义了一个it的迭代器,看上去没有任何问题,但是编译时却报错了。
可以看到此处出现了语法错误,编译器无法确定Continer::const_iterator是什么。
为什么编译器无法判断其是什么呢???
原因:Container没有实例化所以编译器不知道这个容器里面定义了什么。那Container::a,其中a可以是什么???a可以是一个静态变量,一个内部类,也能是一种类型等等,编译器无法识别其具体是什么,所以此处需要指明其是类型,即添加typename的前缀。
typename Container::const_iterator it = _con.begin();
关于array
在C++中添加了一个数组的类array,实际上其与数组没有很大的区别,唯一的区别可能就是array比arr对于越界的检查更严格。
总结
模板既有优点也有缺点。
优点:1)模板服用代码,节省资源,更快的迭代开发;
2)增强了代码的灵活性。
缺点:1)导致代码膨胀问题,编译时间增加;
2)出错时,报错信息非常凌乱,不易定位错误位置。