模板详解
1.函数模板1.概念2.语法3.原理4.实例化1.隐式实例化2.显示实例化 5.匹配原则 2.类模板1.格式2.实例化 3.非类型模板参数注意点 4.特化1.概念2.函数模板特化1.前提2.语法说明3.示例 3.类模板特化1.全特化2.偏特化/半特化3.选择顺序 4.按需实例化 5.模板的分离编译1.分离编译2.模板的分离编译问题分析问题解决方案 3.模板的两次编译 6.总结优点缺点
1.函数模板
1.概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.语法
template<typename T1, typename T2,......,typename Tn>返回值类型 函数名(参数列表){ ...}
template<typename T>void Swap( T& left, T& right){T temp = left;left = right;right = temp;}
3.原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
4.实例化
1.隐式实例化
通过传入参数的类型让编译器自己推理。
2.显示实例化
自己手动写出传入的类型。在函数名后的<>中指定模板参数的实际类型
注意:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
int main(){int a = 10; double b = 20.0; // 显式实例化Add<int>(a, b); return 0;}
5.匹配原则
有现成的(现有的函数重载)使用现成的,没有现成的就使用模板实例化。
注意:模板函数不允许隐式类型转换,但普通函数可以。
int Add(int left, int right){return left + right;}template<class T1, class T2>T1 Add(T1 left, T2 right){ return left + right;}void Test(){ Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化 Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数,而不会进行隐式类型转换}
2.类模板
1.格式
template<class T1, class T2, ..., class Tn>class 类模板名{ // 类内成员定义};
2.实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
vector<int> s1;
3.非类型模板参数
在之前模板参数都是给的类型,比如Int,double之类,但是我们也可以在模板参数之中给一个常量。
namespace test{// 定义一个模板类型的静态数组 template<class T, size_t N = 10> class array {public:T& operator[](size_t index){return _array[index];}const T& operator[](size_t index)const{return _array[index];} size_t size()const{return _size;} bool empty()const{return 0 == _size;}private:T _array[N];size_t _size; };}
比如在实现静态顺序表时,其的空间大小是确定的,使用N来确定,如果需要同时创建一个数组大小为10和大小为1000的顺序表呢?我们只能将N改为1000来满足需求,但是这样会浪费一定的空间。如果可以将常量也可以作为模板参数使用,那么我们分别需要一个数组大小为10和数组大小为1000的顺序表时,可以通过传入参数来确定大小,从而不会浪费多余的空间。
注意点
1.非类型模板参数默认只能给整型家族的,直到C++20以后才支持double,string等
2.非类型模板参数必须在编译的时候就能确定大小因为在编译期就需要开辟空间。
4.特化
由于模板,我们可以实现出与类型无关的代码,来实现相同的功能,但是对于某些类型,使用模板的逻辑会产生我们预期之外的结果,这种类型需要特殊处理,因此产生了特化。
1.概念
在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化
2.函数模板特化
1.前提
必须有函数模板—>模板特化不能单独存在
2.语法说明
template<>返回值类型 函数名<需要特化的类型> (形参1,....){}
注意:函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
3.示例
template<class T>bool Less(T x, T y){cout << "Less(T x, T y)" << endl;return x < y;}template<>bool Less<int*>(int* x, int* y){cout << "Less<int*>(int* x, int* y)" << endl;return *x < *y;}template<class T>bool Less(T* x, T* y){cout << "Less(T* x, T* y)" << endl;return *x < *y;}template<class T>bool Less(T* x, T* y){cout << "Less(T* x, T* y)" << endl;return *x < *y;}int main(){int* a = new int(4);int* b = new int(5);cout << Less(a, b) << endl;double* c = new double(1.1);double* d = new double(2.2);cout << Less(c, d) << endl;return 0;}
总结:尽量不要使用函数模板的特化,因为语法等各种比较复杂,如果有需要,直接函数重载即可。
注意:其调用规则为有重载调用重载,没有重载再看特化中是否有符合的,如果特化中没有调用模板实例化的函数。
3.类模板特化
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;}private: int _d1; char _d2;};
2.偏特化/半特化
偏特化有两种形式:
1.部分参数特化部分特化就是把该类模板的一部分参数确定化。2.参数限制(即类型特化)参数限制特化就是对该类模板的参数符合一定格式要求的确定化。(比如正常类型走模板实例化,指针类型走特化等)
//函数模板template<class T1, class T2>class Data{public:Data() { cout << "Data<T1, T2>" << endl; }private:T1 _d1;T2 _d2;};//参数半特化template<class T1>class Data<T1, char>{public:Data() { cout << "Data<T1, char>" << endl; }private:T1 _d1;char _d2;};//类型限制template<class T1,class T2>class Data<T1*, T2*>{public:Data() { cout << "Data<T1*, T2*>" << endl; }private:T1 _d1;char _d2;};int main(){Data<int, int> d1;Data<int, char> d2;Data<int*, double*> d3;return 0;}
3.选择顺序
有现成的(即特化好的)使用现成的,没有现成的使用模板实例化出来的。
4.按需实例化
按需实例化就是编译器在实例化时用到哪个函数才会实例化哪个函数。
情景:
//array.hnamespace test{// 定义一个模板类型的静态数组template<class T, size_t N = 10>class array{public:T& operator[](size_t index) { size(1); //此处是有一个错误的!!!return _array[index]; }size_t size()const { return _size; }bool empty()const { return 0 == _size; }private:T _array[N];size_t _size = 0;};}//test.cppint main(){test::array<int, 100> a;cout << a.empty() << endl; cout << a.size() << endl; a[10];return 0;}
上面的代码如果运行起来是没有报错的,因为模板是一个半成品,编译器在预处理之后,编译之前会对模板的大体框架进行粗略检查,比如有无分号括号等,但是不会检查内部细节,比如上面size()中参数个数不一致,在创建a这个对象时实例化了默认构造函数,由于我们只调用empty和size(),因此只实例化了这两个函数,没有实例化错误的operator[],就不会报错,除非我们调用它。
5.模板的分离编译
1.分离编译
分离编译就是每一个源文件都是独立编译生成其各自的目标文件,然后所有的目标文件在链接过程中才会整合最后形成可执行文件。
2.模板的分离编译
如果一个类使用了模板,然后其的函数声明和定义分开,声明在.h文件,定义在.cpp文件,那么运行后编译器会报错:无法解析的外部符号。
问题
//array.h#pragma once#include<iostream>using namespace std;namespace Array{// 只支持整形做非类型模板参数// 非类型模板参数 类型 常量// 类型模板参数 class 类型template<class T, size_t N = 10>class array{public:size_t size() const;private:T _array[N];size_t _size = 0;};void func();}//array.cpp#include"array.h"namespace Array{template<class T, size_t N>size_t array<T, N>::size() const{T x = 0;x += N;return _size;}void func(){cout << "I am func" << endl;}}//test.cpp#include"array.h"int main(){Array::array<int, 100> a;cout << a.size() << endl;Array::func();}
运行后报错:
分析问题
出现这种链接错误是因为找不到这个函数的地址。
可是我们已经定义了该函数,为什么会没有它的地址呢?
由于每一个文件都是分离编译生成对应的目标文件,然后再进行链接。
对于func函数,array.h在test.cpp文件中展开,由于有函数的声明,并且其也有定义,在生成符号表时会正确的加入符号表从而被查找到。
对于size函数,array.h在test.cpp文件中展开,其有函数声明,在编译时不会报语法错误,但是在链接时,由于调用了size(),因此需要去符号表找其对应的地址去调用,但是我们的size()定义时只是一个模板,并没有实例化出来,因此在符号表中无法找到对应的函数去调用,因此报链接错误。
那么我们明明已经在test.cpp中实例化了对象,为什么size()没有实例化呢?
因为每一个文件是分离编译的!!!只有test.cpp文件知道array这个类需要用int和100来实例化,但是array.cpp并不知道,因此也不会实例化test.cpp在链接时直接去找自然找不到。
解决方案
1.显示实例化
即在.cpp文件中加入这样的代码:
templatearray<int, 100>;
这样可以显示告诉编译器需要实例化出的案例,这样这样在分离编译时,array.cpp才会实例化出对应的函数。
但是不推荐这样的写法。因为如果这样,那么每一次实例化出类型不同的类,都需要再次进行显示实例化。
2.将声明和定义写在同一个.h中(强烈推荐!!!)
因为array.h在test.cpp中展开时,由于声明和定义在一个文件,展开后也在一个文件,那么自然知道需要实例化成什么类型。
3.模板的两次编译
模板的两次编译是指:
第一次在预处理之后,编译之前,会进行按需实例化,第二次是在编译的时候对实例化的部分进行语法排查。
6.总结
优点
1.模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2.增强了代码的灵活性
缺点
1.模板会导致代码膨胀问题,也会导致编译时间变长
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误