目录
1. 什么是泛型编程
如何实现一个通用的加法函数呢?
//内置类型
int Add(int& x, int& y) { return x + y; }
double Add(double& x, double& y) { return x + y; }
//......
//自定义类型
Date Add(Date& x, Date& y) { return x + y; }
//......
像上面这样,使用函数重载虽然可以实现,但是有以下几个不好的地方:
A. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
B. 代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否告诉编译器一个模板,让编译器根据不同的类型,利用该模子来自己生成代码呢?
答案肯定是可以的。
而这种编程思想就是 泛型编程 :一种编程范式,编写与类型无关的通用代码,是代码复用的一种手段;模板是泛型编程的基础,包含函数模板和类模板。
2. 函数模板
2.1 定义格式
/*
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
*/
//eg:
template<typename T>
T Add(T& x, T& y) { return x + y; }
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
2.2 实例化及原理
即,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用,叫做:隐式实例化。
但是,对于以下情况语句:
int a = 10;
double b = 1.34;
Add(a, b);
该语句不能通过编译,因为在编译期间,需要推演其实参类型,通过实参a将T推演为int,通过实参b将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int 或者 double类型而报错; 因为:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅 。
此时有两种处理方式:
1. 用户自己来强制转化
Add(a, (int)b) 或 Add((double)a, b);
2. 显式实例化 :在函数名后的<>中指定模板参数的实际类型
Add<int>(a, b) 或 Add<double>(a, b>;
此时的模板修改为:
template<class T>
T Add(const T& x, const T& y) { return x + y; }
因为,显示实例化进行了类型转换,生成临时对象,需要const修饰。
小细节: 此时,隐式和显式调用的是同一个函数,更准确的说是:编译器先生成了显式实例化的函数,可供隐式推演直接使用。
原理是:const形参既可以接受const形参,也可以接受非const形参;但非const形参只能接受非const形参。也就是小编之前总结的一句话:权限只能缩写,不能放大!
如下示图:
2.3 参数匹配原则
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例;如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
总之就是:永远选择当前情况下有的且最合适的;如果只有且允许类型转换的,就退而求其次;如果还没有,就模板生成!
3. 类模板
3.1 定义格式
/*
template<class T1, class T2, ..., class Tn>
class 类模板名
{
//类内成员
};
*/
//eg:
template<class T>
class stack
{
public:
//方法......
private:
T* _arry;
int _top;
int _capacity;
};
3.2 实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的模板参数类型放在<> 中即可。
//stack是类名,stack<T>才是类型
stack<int> sta1;
stack<double> sta2;
stack<stack<int>> sta3;
//......
4. 非类型模板参数
前面我们看的都是 类型形参,即: 跟在class/typename之后的T1, T2, ... 代表具体的数据类型。
而非类型形参,是用一个常量作为函数/类模板的一个参数,在编译期就能确认结果,所以在函数/类模板中可将该参数当成常量来使用;但是只支持整形常量:int, unsigned int, size_t
如下示例:
// 定义一个模板类型的静态数组
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; }
//函数模板
template<size_t N = 20>
void func() { cout << N << endl; }
private:
T _array[N];
size_t _size;
};
int main()
{
Array<int, 30> a1;
a1.func<40>();
return 0;
}
5. 模板的特化
5.1 概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型可能会得到一些错误的结果,需要特殊处理。比如:实现了一个专门用来进行小于比较的函数模板。
template<class T>
bool Less(T left, T right) { return left < right; }
int main()
{
cout << Less(1, 2) << 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;
}
可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2里的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化,即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化分为 函数模板特化 与 类模板特化;同一模板特化又可分为 全特化 和 偏特化。
5.2 特化的实现
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template/class后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
所以,为了解决5.1中的示例问题,需要在原基础上增加特化版本的函数,如下:
template<>
bool Less<Date*>(Date* left, Date* right) { return *left < *right; }
【 但是,一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出:bool Less(Date* left, Date* right) { return *left < *right; } 】
类模板的特化步骤和函数模板的特化是一样的,如下示例:
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;
};
int main()
{
Data<int, int> d1;
Data<int, char> d2;
}
输出:
像上面这样,将模板参数列表中所有的参数都确定化就是全特化。包括前面 2.2函数模板的显示实例化 和 3.2类模板的实例化 所有演示示例 都是全特化。
而 偏特化 并不是全特化的对立面,而是: 任何针对模版参数进一步进行条件限制设计的特化版本;有以下两种表现形式:
//基础模板
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data<T1, T2>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//表现形式1:部分特化:将模板参数类表中的一部分参数特化
//将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() {cout<<"Data<T1, int>" <<endl;}
private:
T1 _d1;
int _d2;
};
//表现形式2:参数更进一步的限制
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
int main()
{
Data<double , int> d1; // 调用特化的int版本
Data<int , double> d2; // 调用基础的模板
Data<int *, int*> d3; // 调用特化的指针版本
}
输出:
6. 模板的分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链 接起来形成单一的可执行文件的过程称为分离编译模式。
而对于模板来说,如果出现下面的情况,以类模板为例:
单独的函数模板也是同样的道理。
最好的解决办法就是把声明和定义放到同一个头文件中;当然也可以在模板定义的位置显式实例化一下,但这种方法不实用,所以不推荐使用。
(如果你对上述的编译链接有疑问,可点击前往小编的另一篇文章《程序环境和预处理详解》 )
最后我们简单总结一下:模板的优点:复用了代码,增强了代码的灵活性,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
模板的缺陷:可能会导致代码膨胀问题 和 编译时间变长 ;出现模板编译错误时,错误信息非常凌乱,不易定位错误。
本篇分享到这就结束了,但对你我来说只是学习的又一个新起点;尽管上述的知识点都比较简单,可 “不积硅步,无以至千里”,只有打好基础,才能在未来的更多实际使用场景中游刃有余地解决问题。
当然,如果本篇分享对你有所帮助的话,就是对小编最大的鼓励啦,可以的话,点赞+收藏+评论并分享给你的小伙伴一起学习吧,关注小编,持续更新中!