之前我们讲了函数重载,对一个不同参数类型的函数作出了不同的行为,但是其实有一个更加高效的方式,那就是函数模板,也叫泛型编程。或者模板元编程。
下面是一个交换两个元素的例子
# include "iostream"
template<typename Type>
void swap(Type &a, Type &b) {
Type c = a;
a = b;
b = c;
}
int main() {
int i = 0;
int j = 1;
swap<int>(i, j);
std::cout << i << std::endl;
std::cout << j << std::endl;
}
我们只需要在调用时指定类型就可以
我们也可以指定两种类型,下面的程序没有什么实际意义,就是为了展示一下
# include "iostream"
template<typename Type1, typename Type2>
Type2 add(Type1 &a, Type2 &b) {
Type2 c = a + b;
return c;
}
int main() {
int i = 0;
int j = 1;
int m = add<int,int>(i, j);
std::cout << i << std::endl;
std::cout << j << std::endl;
std::cout << m << std::endl;
}
在C++中,我们没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。
比如上面的add函数,我们传入两个指针,最后返回两个相加的指针地址,这是没有意义的。我们需要将我们用到的数据单独处理,
模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显式具体化(Explicit Specialization)。
我们看下面的例子
#include
#include
using namespace std;
typedef struct{
string name;
float score;
} STU;
template<typename T> const T & Max(const T &a, const T &b){
return a > b ? a : b;
}
template<> const STU & Max<STU>(const STU &a, const STU &b){
return a.score > b.score ? a : b;
}
ostream & operator << (ostream & out, const STU &stu){
out << stu.name << ' ' << stu.score;
return out;
}
int main(int argc, char const *argv[]){
int a = 10, b = 20;
cout<<Max(a, b)<<endl;
STU stu1 = {"Sam", 90}, stu2 = {"Amy", 100};
cout<<Max(stu1, stu2);
return 0;
}
请格外注意这一行
template<> const STU & Max<STU>(const STU &a, const STU &b)
我们使用了STU直接替换了T,也就是显示式的申明了参数类型。Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写作template<>另外,Max中的STU
是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够逆推出 T 的具体类型。简写方式如下所示:
template<> const STU& Max(const STU& a, const STU& b);
#define MAXNAME 128
struct job {
char name[MAXNAME]:
int salary;
};
template<class T>
void swap(T &a, T &b )
{
T temp;
temp = a;
a = b;
b = temp;
};
# 实例化
template void swap<int>(int &a, int & b);
template<> void swap<job>(job &a, job &b) {
int salary:
salary = a.salary:
a.salary = b.salary;
b.salary = salary;
};
为进一步了解模板,必须理解术语实例化和真体化。记住,在代码中包含函数模板本身并不会生成函数:!定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例.( instantiation)。例如,在第一个程序中,函数调用swap(i,j)导致编译器生成swap()的一个实例;:该实.例使用int类型。模板并非函数定义,但使用.int 的模板实例是函数定义::这种实例化方式被称为隐式实例化(implicit instantiation),因为编译器之所以知道需要进行定义,是由于程序调用Swap()函数时提供了 int参数。
模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。
模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。
另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。
具体化:即显式具体化,与实例化不同的是,它也是一个模板定义,但它是对特定类型的模板定义。
**实例化:**在程序中的函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。这即是函数模板的实例化。
有人会说,那具体化不就是实例化基础上多此一举吗?不是的,有时要针对特定数据类型做不同的处理,所以需要具体化。
在程序运行时匹配模板时,遵循的优先级是:具体化模板优先于常规模板,而非模板函数优先于具体化和常规模板。
C++ 支持显式实例化的目的是为「模块化编程」提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:程序员必须要在模板的定义文件(实现文件)中对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。
一个模板可能会在多个文件中使用到,要保持这些文件的同步更新是非常困难的。而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。
总起来说,如果我们开发的模板只有我们自己使用,那也可以勉强使用显式实例化;如果希望让其他人使用(例如库、组件等),那只能将模板的声明和定义都放到头文件中了。
对于函数重载,函数模板和函数模板重载,C++需要有一个策略找出使用哪一个函数定义,尤其是有多个参数时,这个过程称之为重载解析,我们只需要大致了解一下这个过程
在C++发展的早期,大多数人都没有想到模板函数和模板类会有这么强大而有用,它们甚至没有就这个主题发挥想象力。.但聪明而专注的程序员挑战模板技术的极限,阐述了各种可能性。根据熟悉模板的程序员提供的反馈,C++98标准做了相应的修改,并添加了标准模板库。.从此以后,模板程序员在不断探索各种可能性,并消除模板的局限性。C+11标准根据这些程序员的反馈做了相应的修改。下面介绍一些相关的问题及其解决方案。
template <class T1, class T2>
void ft(T1 t1,T2 t2) {
type xpy = t1 + t2;
}
xpy应该是什么类型呢,我们是不知道的,因为我们不知道用户会把T1和T2赋值为什么,所以在C++11标准中新增了关键字decltype。
int s = 11;
decltype(s) ss;
ss的类型和s保持一致。
因此可以这样修复之间的模板函数
template <class T1, class T2>
void ft(T1 t1,T2 t2) {
decltype(t1 + t2) xpy = t1 + t2;
}
decltype使用起来还是比较简单地,但是实现起来确实比较复杂,为了确定类型,编译器必须遍历一个核对表,假设现在有下面的申明
decltype(expression) var;
第一步,如果expression是一个没有用括号括起来的标识符,则var类型与该标识符的类型相同,包括const等限定符。
double x = 5.5;
double &xx = 5.5;
const double *xxx = x;
decltype(x) a; // double
decltype(xx) b; // double &
decltype(xxx) c; // const double *
第二步,如果expression是一个函数调用,则car的类型与函数的返回类型相同,
long fun(int);
decltype(fun(3)) d; // long
这里并不会调用函数,编译器只是看一下
第三步:如果expression是:个左值,则var为指向其类型的引用。这好像意味着前面的a应为引用类型,因为x是一个左值。但别忘了,这种情况已经在第一步处理过了。要进入第三步,expression不能是未用括号括起的标识符,那么,,expiession是什么时将进入第三步呢?种显而易见的情况是,expression是用括号括起的标识符:
double xx = 4.4;
decltype((xx)) e = xx; //e is double &
顺便说一下,括号并不会改变表达式的值和左值性,下面两条语句等价
xx = 3.3;
(xx) = 3.3;
第四步:如果前面的条件都不满足,则 var的类型与expression 的类型相同
int i = 3;
int &k = j;
int &n = j;
decltype(j+6) a; // int
decltype(100L) b; // long
decltype(k+n) c; // int
虽然k与n都是引用,但是k+n并不是引用,他是两个int的和,所以类型为int
有一个问题是decltype所无法解决的,看下面这个函数
template<class T1,class T2>
type gt(T1 t1,T2 t2){
return x+y;
}
同样,无法预先知道将x和y.相加得到的类型。好像可以将返回类型设置为deceltype(x+y),但不幸的是,此时还未声明参数x和y,它们不在作用域内,(编译器看不到它们,也无法使用它们)。必须在声明参数后使用decltype,为此,C++新增了三种声明和定义函数的语法。主面使用内置类型来说明这种语法的王作原理。对于下面的原型:
double h(int x,float y);
使用新增的语法可以写成这样
auto h(int x,float y) -> double;
将返回类型移到参数申明后面,->double称为后置返回类型,其中auto是一个占位符,表示后置返回类型提供的类型,这样我们的模板就可以改为
template<class T1,class T2>
auto gt(T1 t1,T2 t2) -> decltype(x+y){
return x+y;
}