标题:[C++]: 模板进阶
@水墨不写bug
目录
一、非类型模板参数
(1)、非类型模板参数简介
(2)、非类型模板参数实例
二、模板的特化
(1)函数模板特化
(2)类模板特化
三、模板的分离编译
正文开始:
一、非类型模板参数
(1)、非类型模板参数简介
在模板初阶中,我们讲解了一般我们使用模板的做法:
//函数模板
template<class T1,class T2>
void func(T1 t1,T2,t2)
{
//......
}
//类模板
template<class T>
class A
{
public:
//......
private:
T t;
}
模板在如下场景中的使用会让你感到更加方便:
如果我们要实现一个栈,在通常情况下我们可能会选择实现一个静态的栈,它的大小是固定的,比如:
typedef N 100
template<class T>
class stack
{
private:
T _data[N];
}
但是我们发现这样实现的栈的局限性很大,因为一旦确定它的大小,就无法改变了。如果我们想要在实例化的时候能够自己手动确定它的大小,就需要用到非类型模板参数;
模板参数的类型分为:类型模板形参与非类型模板形参。
类型模板形参:出现在模板的参数列表中,跟在class或者typename之后;
非类型模板形参:就是用一个常量作为类(函数)模板的一个参数,在实例化的时候确定,在模板内部可以将该参数作为常量来使用。
在使用非类型模板形参之后,我们可以这样定义模板:
template<class T, int N>
class stack
{
public:
private:
T st[N];
};
int main()
{
stack<int, 100> st1;
stack<int, 10> st2;
return 0;
}
其中,第二个模板参数 是一个整形常量,这个常量值在类实例化的时候确定。这样一来,就可以在创建栈的时候定义它的大小。
注意:
1.浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2.非类型模板参数必须在编译时就能确定。
但是,在C++20及以后,浮点数可以作为类的非类型模板参数 。
(2)、非类型模板参数实例
STL中有一种容器,array;
在C++11及以后,它就是一种使用非类型模板参数的容器:
array就是数组,但是它是一种封装后的一种数组;对于一般的数组,越界检查是部分的抽查,是通过编译器内部对比数组边界外的小范围内是否被改变来检测实现的;
如果我们只读,检测不出来:
int main()
{
int a[10] = { 1,2,3,4,5,6,7,8,9,10};
a[10];
a[11];
a[12];
return 0;
}
如果我们越界写入一个值,就会被检测出来;甚至编译都无法通过:
但是当我们在数组外距离边界比较远的地方越界写入时,就可能不会被检测出来:
但是array解决了这个问题,因为array是一个类,它可以通过在类内部实现对 [ ] 的重载来进行严格的越界检查,也就是通过assert()来进行检查。
在使用容器array时越界访问,在越界读时会被检测出来:
#include<array>
using namespace std;
int main()
{
array<int, 10> arr = {1,2,3,4,5,6,7,8,9,10};
arr[10];
return 0;
}
越界写时也可以检测出来:
#include<array>
using namespace std;
int main()
{
array<int, 10> arr = {1,2,3,4,5,6,7,8,9,10};
arr[10] = 1;
return 0;
}
二、模板的特化
我们曾将实现了一个比较大小的函数模板:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
这个模板在大多数情况下都可以正常使用,不会出错,比如对int(整形家族),Date(重载了比较运算符的自定义类型等)都可以正常使用:
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0);
bool operator<(Date d) const;
private:
int _year = -1;
int _month = -1;
int _day = -1;
};
template<class T>
bool Less( T left, T right)
{
return left < right;
}
int main()
{
int a = 1;
int b = 2;
cout << ::Less<int>(a, b) << endl;
cout << ::Less<double>(9.2, 8.2) << endl;
Date d1(2022, 1, 1);
Date d2(2023, 1, 1);
cout << ::Less<Date>(d1, d2) << " ";
return 0;
}
但是对于一种特殊情况,Less函数就会出现问题了:
int main() { Date d1(2022, 1, 1); Date d2(2023, 1, 1); cout << ::Less<Date*>(&d1, &d2) << " "; return 0; }
运行结果会随着d1和d2的实例化顺序而不同:
究其原因,是函数模板实例化出的函数是根据d1和d2的地址大小比较的而不是d1和d2本身。
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理 ,这就需要用到 模板的特化 ;
对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
(1)函数模板特化
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同!
我们就以上面的less函数模板特化出Date类函数为例,进行模板特化:
template<class T> bool Less(T left, T right) { return left < right; } template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; } int main() { Date d2(2023, 1, 1); Date d1(2022, 1, 1); cout << ::Less<Date*>(&d1, &d2) << " "; return 0; }
这样我们就完成了对函数模板的特化,这时,函数就会根据d1和d2大小来比较,而不是根据d1和d2的地址来比较。这样函数的结果就不会因为d1和d2的实例化顺序不同而产生差异了。
但是,我们在实现一个函数的时候,传参传自定义类型消耗太大,于是就需要传引用,既然传引用就要防止对象被改变,需要加上const,如下:
template<class T> bool Less(const T& left,const T& right) { return left < right; }
当你试图对这个函数模板进行特化的时候,就会发现意想不到的问题:刚开始学习特化,有一个误区:那就是将原模板的类型直接替换到特化后的函数内,比如对上面这个函数进行特化<Date*>,可能你会这样写:
template<> bool Less<Date*>(const Date*& pleft,const Date* & pright) { return *pleft < *pright; }
但是,这是错误的写法;你会发现编译都无法通过,这就是违背了“4. 函数形参表: 必须要和模板函数的基础参数类型完全相同!”的这一条规则。
仅仅对于语法来说,对于less模板,const修饰的两个变量本身不能修改,特化的Date*版本,两个参数是指针类型,要与模板保持一致,就需要const修饰变量本身,即const修饰指针本身。
并且const在*之前,修饰指针的内容;const在*之后,修饰指针本身。那么这样写才是正确的:
template<> bool Less<Date*>(Date* const& pleft, Date* const& pright) { return *pleft < *pright; }
这一点需要非常慎重,特别注意!
同时你可能会发现:
const修饰指针本身,但是我们还可以通过指针解引用改变其内容,这不是我们希望的,这也就要求:当你在使用函数模板的时候,需要对特化出来的函数一清二楚,并不能说你试着特化一下,看一下特化出来的东西是不是想要的,这个不是概率问题。
(2)类模板特化
类模板的特化分为全特化和偏特化。
全特化:即是将模板参数列表中所有的参数都确定化:
template<class T1,class T2> class Data { private: T1 t1; T2 t2; }; template<> class Data<char, int> { private: char t1; int t2; };
偏特化:任何针对模版参数进一步进行条件限制设计的特化:
对参数的进一步限制可以是对参数部分特化:// 将第二个参数特化为int template <class T1> class Data<T1, int> { private: T1 _d1; int _d2; };
也可以是对参数类型的进一步限制:
//两个参数偏特化为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> { private: T1 _d1; T2 _d2; };
//两个参数偏特化为引用类型 template <typename T1, typename T2> class Data <T1&, T2&> { private: const T1 & _d1; const T2 & _d2; };
对于上述两种偏特化类型,可能会有一个误区:
我们直接避开这个误区不谈,直接将正确的思想;其实,上述两个类名后面的特化参数只是一个标记,编译器会根据这个标记来匹配特化的类,而不会由于特化参数的写法而改变原本传入的参数类型:
比如:
template<class T1,class T2> class Data { public: Data() { cout << "原模板" << endl; } private: T1 _d1; T2 _d2; }; //两个参数偏特化为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> { public: Data() { cout << "Data<T1*, T2*>" << endl; } private: T1 _d1; T2 _d2; }; //引用偏特化 template <typename T1, typename T2> class Data <T1&, T2&> { public: Data(const T1& d1 = 0, const T2& d2 = 0) : _d1(d1) , _d2(d2) { cout << "Data<T1&, T2&>" << endl; } private: const T1& _d1; const T2& _d2; }; int main() { Data<int, int> d1; Data<int*, double*> d2; Data<int&, int&> d3; return 0; }
指针偏特化,传入<int*,double*>,参数T1,T2分别就是int*,double*,不会因为类名后面的<T1*, T2*>而将T1,T2改变为int,double。
引用偏特化也是类似的。
三、模板的分离编译
一个程序(项目)由若干个文件共同组成,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
当我们在使用模板时,如果使用分离编译模式,会导致链接错误。
由于模板在编译时不会实例化出对应的类或者函数,自然没有相应的地址。所以在链接时,编译器在找这个类或者函数的地址时,会找不到,所以报错。
解决方法:
1. 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
模板总结
【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
完~
未经作者同意禁止转载