测试环境:Target: x86_64-linux-gnu
gcc version 5.3.1 20160413 (Ubuntu 5.3.1-14ubuntu2.1)
什么是泛型编程?为什么C++会有模板?这一切的一切都要从如何编写一个通用的加法函数说起。
很久很久以前
有一个人要编写一个通用的加法函数,他想到了这么几种方法:
使用函数重载,针对每个所需相同行为的不同类型重新实现它
int Add(const int &_iLeft, const int &_iRight)
{
return (_iLeft + _iRight);
}
float Add(const float &_fLeft, const float &_fRight)
{
return (_fLeft + _fRight);
}
当然不可避免的有自己的缺点:
1、只要有新类型出现,就要重新添加对应函数,太麻烦
2、代码的复用率低
3、如果函数只是返回值类型不同,函数重载不能解决(函数重载的条件:同一作用域,函数名相同,参数列表不同)
4、一个方法有问题,所有的方法都有问题,不好维护
· 使用公共基类,将通用的代码放在公共的基础类里面
【缺点】
1、借助公共基类来编写通用代码,将失去类型检查的优点
2、对于以后实现的许多类,都必须继承自某个特定的基类,代码维护更加困难
宏
#define ADD(a, b) ((a) + (b))
1、不进行参数类型检测,安全性不高
2、编译预处理阶段完成替换,调试不便
所以在C++中又引入了泛型编程的概念。泛型编程是编写与类型无关的代码。这是代码复用的一种手段。模板则是泛型编程的基础。
模板分为了函数模板和类模板:
函数模板
函数模板:代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
什么意思呢?往下看就知道了。
模板函数的格式
template <typename T> //T可是自己起的名字,满足命名规范即可
T Add(T left, T right)
{
return left + right;
}
int main()
{
Add(1, 2); //right 调用此函数是将
Add(1, 2.0); //error 只有一个类型T,传递两个不同类型参数则无法确定模板参数T的类型,编译报错
return 0;
}
对第一个函数调用,编译器生成了 int Add(int, int) 这样一个函数。
typename是用来定义模板参数关键字,也可以使用class。不过建议还是尽量使用typename,因为这个关键字是为模板而生的!
注意:不能使用struct代替typename。(这里的class不是之前那个class的意思了,所以你懂的)
当然你也可以把函数模板声明为内联的:
template <typename T>
inline T Add(T left, T right) {//…}
实例化
编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为函数模板实例化。(用类类型来创建一个对象也叫做实例化哦!)
模板的编译
模板被编译了两次:
1、实例化之前,检查模板代码本身,查看是否出现语法错误,如:遗漏分号(遗憾的是不一定能给检查的出来)
2、在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持某些函数调用
实参推演
从函数实参确定模板形参类型和值的过程称为模板实参推演。多个类型形参的实参必须完全匹配。
如对这样的函数调用:
template <typename T1, typename T2, typename T3>
void fun(T1 t1, T2 t2, T3 t3)
{
//do something
}
int main()
{
fun(1, 'a', 3.14);
return 0;
}
编译器生成了如下这样的函数:
其中,函数参数的类型是和调用函数传递的类型完全匹配的。
类型形参转换
一般不会转换实参以匹配已有的实例化,相反会产生新的实例。
举个栗子:对如下的函数调用:
template <typename T>
T Add(T left, T right)
{
return left + right;
}
int Add(int left, int right)
{
return left + right;
}
int main()
{
Add(1.2, 3.4);
return 0;
}
即程序中已经实现过了Add函数的int版本,那么调用Add(1.2, 3.4);时,编译器不会将1.2和3.4隐式转换为int型,进而调用已有的Add版本,而是重新合成一个double的版本:
当然前提是能够生成这么一个模板函数。如果这个模板函数无法生成的话,那么只能调用已有的版本了。