背景引入:
想象一下,我们要实现一个整数相加,浮点数相加的函数,如果按C语言的思路,我们需要写两个函数名不同的函数,来完成相加;如果C++语言,则可以通过函数重载的特性,写两个函数名相同,但参数列表不同的函数,来完成任务。
不管怎么样,我们都需要实现两个函数?
那么我们可以只写一个通用的函数,来完成这个任务吗?
接下来就要请模板出场!
1. 模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特性类型版本。
2. 使用方法
使用如下方法定义函数模板,并且可以根据需要定义多个模板参数。
template<class A, class B…>
template<class T> //template<typename T> 也可以 T Add(T x, T y) { return x + y; }
3. 模板原理
在编译器编译阶段,对于函数模板的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
例如:当使用double类型使用函数模板时,编译器通过对实参推演,会自动生成一份double类型的函数,以供调用。
template<class T> //template<typename T> 也可以 T Add(T x, T y) { return x + y; } int main() { int a = 10; int b = 11; double c = 1.1; double d = 2.2; Add(a, b); Add(c, d); return 0; }
从上面的例子可以看出,编译器确实自动生成了两个参数不同的函数(因为调用地址不同)。
4. 模板实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
4.1 隐式实例化
让编译器根据实参类型,自己推演。
template<class T> //template<typename T> 也可以 T Add(T x, T y) { return x + y; } int main() { int a = 10; int b = 11; double c = 1.1; double d = 2.2; Add(a, b); // 生成整形模板 Add(c, d); //生成double模板 Add(a, d); // 编译报错,因为a是int,b是double,只有一个模板T,编译器不知道生成哪个。 // 解决方法: //1.用户自己来强制转化 Add(a, (int)d); //2. 使用显示实例化 return 0; }
4.2 显式实例化
在函数名后的<>中指定模板参数的实际类型。
int a = 10; double b =1.1; Add<int>(a, b); //显示实例化 // 如果类型不配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器会自动报错。
5. 模板参数匹配规则
- 一个非模板参数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T> T Add(T left, T right) { return left + right; } void Test() { Add(1, 2); // 与非模板函数匹配,编译器不需要特化 Add<int>(1, 2); // 调用编译器特化的Add版本 }
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时优先调用非模板函数而不会从模板特化一个实例;如果模板可以产生一个更合适的函数,那么选择模板。
// 专门处理int的加法函数 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函 数 }
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
6. 类模板
6.1 定义格式
类模板不是真正的类,而实例化的结果才是真正的类。
template<class T1, class T2, ..., class Tn> class A { // 类内成员定义 };
实例化一个类型为A的a对象,类型要求为int。
A<int> a; // A 类名,A<int> 才是类型
7.模板参数
- 类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
- 非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
namespace xty { 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() { return _size; } bool empty()const { return _size == 0; } private: T _array[N]; size_t _size; }; } int main() { xty::array<int, 20> a1; xty::array<int> a2; return 0; }
该段代码能够成功编译,并且我们通过_array[N],也能感受到,模板参数N是一个常量,并且该位置必须是整型常量(int, size_t, long, long long, char)。
注意:
浮点数、类对象以及字符串是不允许作为非类型模板参数的。
非类型的模板参数必须在编译器就能确认结果。
8.模板特化
模板的特殊化处理,即给模板参数进行进一步的限制或者给模板参数赋值具体的类型。
分为:函数模板特化和类模板特化
首先看一个例子:
class Date { public: 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); } friend ostream& operator<<(ostream& _cout, const Date& d) { _cout << d._year << "-" << d._month << "-" << d._day; return _cout; } private: int _year; int _month; int _day; }; template<class T> bool Less(T left, T right) { return left < right; } int main() { cout << Less(1, 10) << endl; //可以比较结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout << Less(d1, d2) << endl; //可以比较结果正确 Date* p1 = &d1; Date* p2 = &d2; cout << Less(p1, p2) << endl; // 可以比较,结果错误 return 0; }
首先这种情况,只有一个类型的比较函数肯定是不够的,我们可以使用仿函数来解决这个问题。但是这样会造成创建对象的时候多写一个参数。现在我们可以有一个新的方式来解决这个问题。请继续往下阅读:
8.1函数模板特化
函数模板特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后面跟一对尖括号,尖括号中要指定特化的类型。
- 函数形参列表:必须要和模板函数的基础参数类型完全相同。
如下改正即可解决上面的问题:
// 基础模板 template<class T> bool Less(T left, T right) { return left < right; } //特化模板 template<> bool Less<Date*>(Date* left, Date* right) { return *left < *right; }
但是上面的形式很明显有一点冗余,我们可以使用函数重载同样能够解决问题。因此我们不建议函数模板进行特化。
改正版本如下:
// 基础模板 template<class T> bool Less(T left, T right) { return left < right; } 特化模板 //template<> //bool Less<Date*>(Date* left, Date* right) //{ // return *left < *right; //} //函数重载 bool Less(Date* left, Date* right) { return *left < *right; }
8.2 类模板特化
8.2.1全特化
全特化是将模板参数列表中所有的参数都确定化。
如下示例:
template<class T1, class T2> class Xxx { public: Xxx() { cout << "Xxx<T1, T2>"; } private: T1 _t1; T2 _t2; }; //全特化 template<> class Xxx<char, int> { public: Xxx() { cout << "Xxx<char, int>"; } private: char _t1; int _t2; }; void Test() { Xxx<int, int> d1; Xxx<char, int> d2; }
8.2.2偏特化
偏特化:任何针对模板参数进一步进行条件限制设计的特化版本。分为:部分特化和参数更进一步限制的特化。
//基础模板 template<class T1, class T2> class Xxx { public: Xxx() { cout << "Xxx<T1, T2>"<<endl; } private: T1 _t1; T2 _t2; }; // 部分特化,只特化模板参数列表中的一部分参数 template<class T1> class Xxx<T1, int> { public: Xxx() { cout << "Xxx<T1, int>" << endl; } private: T1 _t1; int _t2; }; //对参数进行更一步的限制 //偏特化为指针类型 template<class T1, class T2> class Xxx<T1*, T2*> { public: Xxx() { cout << "Xxx<T1*, T2*>" << endl; } private: T1 _t1; T2 _t2; }; //两个参数偏特化为引用类型 template<class T1, class T2> class Xxx<T1&, T2&> { public: Xxx(const T1& d1, const T2& d2) :_t1(d1) ,_t2(d2) { cout << "Xxx<T1&, T2&>" << endl; } private: const T1& _t1; const T2& _t2; }; void Test() { Xxx<double, int> d1; //调用特化的int版本 Xxx<int, double> d2; //调用基础的模板 Xxx<int*, int*> d3; //调用特化的指针版本 //传进去的1, 2为常量,不能改变,需要用const接收 Xxx<int&, int&> d4(1, 2); // 调用特化的引用版本 }
9.模板分离编译
9.1 分离编译概念
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程称为分离编译模式。
9.2 模板的分离编译
先给结论:模板不能分离编译,即模板的声明在a.h文件中,模板的定义在a.cpp文件中,编译不通过!
如下示例:
//a.h template<class T> T Add(const T& left, const T& right); void Func(); //a.cpp template<class T> T Add(const T& left, const T& right) { return left + right; } void Func() { std::cout << "Func()" << std::endl; } //main.cpp int main() { Add(1, 2); Add(1.1, 2.2); Func(); return 0; }
分析:
c/cpp程序运行,要经历:预处理->编译->汇编->链接
预处理:.i文件,宏替换,头文件替换。
编译:.s文件,对程序进行词法、语法、语义分析,检查无误后生成汇编代码,注意头文件不参与编译,编译器对工程中的多个源文件时分离开单独编译的。
汇编:是.o文件,各个.o文件会生成符号表。
链接:将多个obj文件合并成一个(通过符号表的地址去找),并处理没有解决的地址问题。
因此普通函数可以编译通过,但声明定义分离的模板就不能编译通过。
9.3 解决办法
- 将声明和定义的位置放到一个文件“xxx.hpp”或者xxx.h也可以。
- 在模板定义的位置显示实例化。不太实用,推荐方法1。
方法一举例:
//a.hpp文件改成这样 template<class T> T Add(const T& left, const T& right); void Func(); template<class T> T Add(const T& left, const T& right) { return left + right; } void Func() { std::cout << "Func()" << std::endl; } //声明和第一放在一起,直接就可以实例化,编译时就有地址,不需要链接
方法二举例:
//a.cpp文件改成这样 template<class T> T Add(const T& left, const T& right) { return left + right; } void Func() { std::cout << "Func()" << std::endl; } //显示实例化 template double Add<double>(const double& left, const double& right); template int Add<int>(const int& left, const int& right);
最粗暴的方法:
//main.cpp直接将模板声明和定义放在main.cpp中 template<class T> T Add(const T& left, const T& right); template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { Add(1, 2); //直接生成地址,不需要链接去找 Add(1.1, 2.2); Func(); //此时func()需要链接上地址,去找定义,才能运行。 return 0; }
总结
优点:
- 模板服用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
- 增强代码的灵活性
缺点:
- 模板会导致代码膨胀问题,一份模板会实例化出好多代码,也会导致编译时间变长。
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误!