目录
1.非类型模板参数
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。请看下面的例子:
(这里我们举例一个静态数组,静态数组是编译器自带的一个容器,只需要包含头文件<array>就可以了,在这里我们只是用它举一个例子,顺便一提:vector完美的包含了array的功能,array挺没用的)。
例如参数N就是一个人非类型模板参数。namespace myarray { template<class T, size_t N = 10> class array { public: T& operator[](size_t index) { assert(index < N); return _array[index]; } size_t size()const { return _size; } bool empty()const { return 0 == _size; } const T& operator[](size_t index)const { return _array[index]; } private: T _array[N]; size_t _size; }; }
在这里非类型模板参数的功能就是,可以通过传递参数去改变array中数组的大小,使这个类更加的灵活。
void test() { myarray::array<int> a1; // 10 myarray::array<int, 1000> a2; // 1000 myarray::array<int, 100> a3; // 100 }
就如上面这个例子,我们可以给类传递不同的大小,进而控制类实例化对象的空间。实际上,这种写法是生成了三个类(由编译器完成),非类型模板参数只是将由编译器完成的重复的工作交给了编译器完成,由编译器创建了这三个类。
2.按需实例化
请看这段代码:我们在编译器设置了一个语法错误(size(1);,size并没有接收参数)
namespace myarray { template<class T, size_t N = 10> class array { public: T& operator[](size_t index) { assert(index < N); size(1); return _array[index]; } size_t size()const { return _size; } bool empty()const { return 0 == _size; } const T& operator[](size_t index)const { return _array[index]; } private: T _array[N]; size_t _size; }; }
下面我们来执行这一段代码:
void test2() { myarray::array<int, 10> a1;// 越界读,检查不出来 }
我们发现编译器并没有报错,正常运行,这是为什么呢?
这是因为,实例化这个类的时候,会按需实例化(调用哪个成员函数就实例化哪个),在这里因为没有调用operator[],所以operator[]有调用参数不匹配,没有检查出来。
如果我们调用了operator[]呢?
eg:
void test2() { myarray::array<int, 10> a1;// 越界读,检查不出来 a1[1]; }
这时候就调用了operator[],编译器进行了语法检查。
3. 模板的特化
我们创建了一个日期类,并给定了日期的比较逻辑,并且我们创建了仿函数进行对象间的比较。
class Date { public: friend ostream& operator<<(ostream& _cout, const Date& d); Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} bool operator<(const Date& d)const { return (_year < d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day); } bool operator>(const Date& d)const { return (_year > d._year) || (_year == d._year && _month > d._month) || (_year == d._year && _month == d._month && _day > d._day); } private: int _year; int _month; int _day; }; ostream& operator<<(ostream& _cout, const Date& d) { _cout << d._year << "-" << d._month << "-" << d._day; return _cout; } //函数模板 template<class T> bool Less(T left, T right) { return left < right; }
请看下面这个例子:
void test1() { cout << Less(1, 2) << endl; // 可以比较,结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; // 可以比较,结果正确 Date* p1 = new Date(2022, 7, 7); Date* p2 = new Date(2022, 7, 8); cout << Less(p1, p2) << endl; // 可以比较,结果错误 }
运行后发现,最后的一组比较结果是错误的。这是因为我们传递的是对象的地址,如果走上面的函数模板,那就是对地址进行比较,当然就错了。
该怎么办呢?
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
3.1函数模板的特化
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
//函数模板 template<class T> bool Less(T left, T right) { return left < right; } //特化(本质是一种重载) template<> bool Less(Date* left, Date* right) { return *left < *right; }
这时候在执行之前的比较代码就全都正确了(最后一组比较就会去走特化了,函数模板只是半成品,现在有成品了,编译器当然就会去走特化了)。
3.2类模板特化
1.全特化
全特化即是将模板参数列表中所有的参数都确定化。
// 类模板 template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 全特化 template<> class Data<int, char> { public: Data() { cout << "Data<int, char>" << endl; } }; void test1() { Data<int, int> d1; Data<int, char> d2; }
符合特化的,直接走特化。
2.偏特化
任何针对模版参数进一步进行条件限制设计的特化版本。
// 类模板 template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 全特化 template<> class Data<int, char> { public: Data() { cout << "Data<int, char>" << endl; } }; //偏特化 template<class T1> class Data<T1, char> { public: Data() { cout << "Data<T1, char>" << endl; } }; void test1() { Data<int, int> d1; Data<int, char> d2; Data<char, char> d3; }
编译器会走匹配自己的情况:
再看一个示例:
// 1.类模板 template<class T1, class T2> class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 2.全特化 template<> class Data<int, char> { public: Data() { cout << "Data<int, char>" << endl; } }; //3.偏特化 template<class T1> class Data<T1, char> { public: Data() { cout << "Data<T1, char>" << endl; } }; //4.偏特化 template<class T1, class T2> class Data<T1*, T2*> { public: Data() { cout << "Data<T1*, T2*>" << endl; } }; //5.偏特化 template<class T1, class T2> class Data<T1&, T2*> { public: Data() { cout << "Data<T1&, T2*>" << endl; } }; void test1() { Data<int, int> d1; Data<int, char> d2; Data<char, char> d3; Data<char*, char*> d4; Data<int*, string*> d5; Data<int*, string> d6; Data<int&, string*> d7; }
两个参数都是指针类型就走4;第一个参数是引用,第二个参数是指针就走5。
4.模板的分离编译
分离编译是什么?
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模板的分离编译?
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// a.h template<class T> T Add(const T& left, const T& right); // a.cpp template<class T> T Add(const T& left, const T& right) { return left + right; } // main.cpp #include"a.h" int main() { Add(1, 2); Add(1.0, 2.0); return 0; }
分析:总的来说就是,调用的地方,知道实例化T成什么类型,但是只有函数声明,没有定义。定义的地方,不知道实例化T成什么类型,所以有定义无法实例化,也就是无法生成函数的地址到符号表。
如何解决?
1. 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
因为.h预处理展开后,实例化模板时,既有声明又有定义,直接就实例化。编译时,有函数的定义,直接就有地址,不需要链接时再去找。
2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
5.模板总结
【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误