函数模板
您是否还在为了需要对多种数据进行相同处理而需要编写许多相同的函数而苦恼?您是否还在为了不确定合适的返回类型又无法编写返回不同的具有相同特征标的函数而苦恼? 快来试试这一章函数模板的内容吧!!
函数模板是通用的函数描述,使用泛型来定义函数,其中的泛型可以用特定的类型来替换,通过将类型作为参数传递给模板,生成相应类型的函数,由于类型是用参数表示的,因此模板特性有时候也被称为参数化类型。
实现
eg:
int main(void)
{
int a,b;
float c,d;
double e,f;
}
对于上面代码来讲,如果我们要交换a和b的值,我们可以构造一个void f(int a,int b);
的函数,但是,当我们需要交换c和d或者e和f的值时,由于数据类型的不同,即使将进行完全相同的操作,但是仍然需要构造一个或者重载一个新的函数,这样的话,将会浪费时间,但是我们可以使用函数模板来解决这个问题:
函数模板的模板
下面以实现一个交换两个变量的值的函数为例:
template <typename identifier>
void swap(identifier &a,identifier &b)
{
identifier temp=a;
a=b;
b=temp;
}
解释:
- 第一行说明下面要建立一个函数模板,并且将类型名称命名为
identifier
对于关键字template
和typename
是必须的,identifier
可以根据标识符命名规则自由发挥。 - 函数模板并不创建任何函数,而是告诉编译器要如何定义函数,等到需要的时候再按相应的数据类型创建函数。
- 在标准C++98之前,未添加关键字
typename
,而是用关键字class
来创建模板。以下这种形式也是正确的:
template <class identifier>
void swap(identifier &a,identifier &b)
{
identifier temp=a;
a=b;
b=temp;
}
typename
这一关键字使得参数identifier
表示类型这一特点更加明显,在大量代码库中,使用关键字class
来开发,在这种上下文中,二者是等价的。
如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字
typename
而不使用class
。
模板的声明和使用
在上面,展示了模板的定义式声明,模板也可以先声明再定义,下面的代码展示了模板的先声明再定义和模板函数的调用方式:
可以看出:
- 在声明后定义时也应该带上
template <typename T>
表示下面的函数定义是一个函数模板。 - 在使用时直接按照正常的函数调用即可。
需要说明: 函数模板不会缩短可执行程序,最后还是生成了相应数量的独立函数来实现函数功能。更常见的情形是把函数模板放在头文件中,便于移植和使用。
先不想后继续讨论,或许我们会有一个问题,上面的代码我们确实实现了对于两个int
和两个float
等数据类型的交换,但是怎么实现对一个float
与一个int
类型的处理呢?(交换对于两个不同类型的数据不太合适)
template <class T1,class T2>
void f1(T1 &a,T2& b)
{
cout<<a*b;
}
这样也是可行的。
重载的模板
根据上一节学习的函数重载内容,可以将函数重载和模板结合到一起。
template <class T>
void swapp(T &a,T &b);
template <class T>
void swapp(T *a,T *b,int c);
当函数调用参数是两个int(或者其他合适)值时,调用第一个模板,当参数为数组和一个int
类型值时,符合第二个函数模板。
以此带来的,我们会发现,虽然方便了函数功能的实现,但是函数调用究竟匹配的是哪一个函数呢?
比如有以下函数定义:
template <class T>
void swapp(T *a,T *b);
template <class T>
void swapp(T a,T b);
那么当使用函数调用swaap(&a,&b);
(其中a,b是int类型变量)时,将调用哪一个函数呢?
- 如果调用第一个显然是没有问题的。函数本来就接受指针。
- 如果调用第二个,那么可以将T替换为
int *
来创建合适的函数。
这么看来,两个函数都是匹配的,那么这种情况,又或是其他一系列情况引发了一个问题,怎么确认匹配的函数呢?后面将会讨论到。
显式具体化
假设有以下代码:
template <class T>
void swapp(T a,T b)
{
......
}
在这个代码中实现了诸如< > + - * /
等运算,如果函数接受的是int double float
等基本数据类型,那么显然是没有问题的,但是当传递的是结构,数组,string类等数据类型,那么上面的运算符或许将不再适用。下面是两种解决方案:
- 重载运算符,以实现所想要进行的操作。
- 为特定类型提供具体化的模板定义。
对于运算符的重载将在类使用中进行讨论。
下面来看看第二种方案:
显式具体化
假设定义了结构:
struct job{
char name[40];
double x;
int y;
};
下面对于交换这个结构的后两个变量进行函数定义。
由于此功能实现无法使用之前的swapp函数模板,所以可以提供一个具体化函数定义——称为显式具体化,当编译器找到与函数调用匹配的具体化定义时,将使用该定义而不再寻找模板。
第三代具体化
C++98标准提供了以下的方法:
- 对于给定的函数名,可以有非模板函数,模板函数和显式具体化函数以及他们的重载版本。
- 显式具体化的原型和定义应以
template<>
打头,并通过名称来指出了类型。 - 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
也就是说可以有如下定义:
void swapp(int &a,int &b);
template <class T>
void swapp(T& a,T& b);
template<> void swapp<job>(job& a,job& b);
根据上述说明顺序来确定具体使用哪一个函数。
上面显式具体化中swapp中的是可选的,因为参数已经表明类型是job,所以也可以这样写:
template <> void swapp(job &a,job &b);
来看一个例子:
#include<iostream>
#include<string>
using namespace std;
struct job{
string name;
double x;
int y;
};
template <class T>
void swapp(T &a,T &b);
template <> void swapp(job &a,job &b);
void show(job a);
int main(void)
{
job a={"lao liu",10.852,456};
job b={"gou ba",0.056,23};
int x=100,y=200;
cout<<x<<" "<<y<<endl;
swapp(x,y);
cout<<x<<" "<<y<<endl;
show(a);
show(b);
swapp(a,b);
show(a);
show(b);
while(cin.get()!=EOF);
return 0;
}
template <class T>
void swapp(T &a,T &b)
{
T temp=a;
a=b;
b=temp;
}
template <> void swapp(job &a,job &b)
{
swapp(a.x,b.x);
swapp(a.y,b.y);
}
void show(job a)
{
cout<<"\n"<<a.name<<endl<<a.x<<endl<<a.y<<endl;
}
下面是运行结果:
实例化和具体化
前面已经提到过,在代码中包含模板并不会生成函数定义,她只是一个用于生成函数定义的方案。
编译器使用模板生成函数定义时,得到的是模板实例化。
最初编译器只能通过隐式实例化来使用模板生成函数定义,现在也可以使用显式实例化来为函数生成定义。这意味着可以直接命令编译器生成特定类型的实例。
显式实例化示例:
template void swapp<int>(int &a,int &b);
编译器执行到这里时,将自动创建一个接受int类型值的显式实例化函数,上述声明的意思是:使用swapp模板生成int类型的函数定义。
这一点与显式具体化不同:
template<> void swapp<job>(job& a,job& b);
template<> void swapp(job& a,job& b);
显式具体化使用上述两种形式,表明:不要使用swapp函数模板来生成函数定义,而应该使用此专门为int类型定制的函数定义。
-
显式具体化需要再有函数定义,而显式实例化使用原模板生成特定函数定义,无需再有定义。
-
在同一个文件或者转换单元中使用同一种类型的显式实例化和显式具体化将出错。
![](https://i-blog.csdnimg.cn/blog_migrate/a5b40e4e635de09a2881fd6ca48c05fd.png)
但是前面把这里面的显示实例化和显式具体化模板声明位置调换却可以通过编译??不知道为什么。
- 可以直接通过函数调用创建显式实例化。
template <calss T>
T add(T a,T b)
{
......
}
......
int m=6;
double n=8;
cout<<add<double>(m,n)<<endl;
如上所示,这样是可行的。
其中表明接受两个double类型参数,然后强制转化接受的类型,以便于与参数列表匹配。
函数调用时究竟使用哪一个函数版本呢?
通过函数模板、函数重载和函数模板重载,有了很多函数,对于如此多的版本的同名函数,C++需要有一个良好的策略来决定什么时候使用哪一个函数。这个过程被称为重载解析。
- 第一步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float 参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float 生成一个实例。
- 第三步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
void may(int);//#1
float may(float,float=3);//#2
void may(char);//#3
char *may(const char *);//#4 排除
char may(const char &);//#5
template<class T> void may(const char &);//#6
template<class T> void may(T*);//#7 排除
may('c');
以上是筛选出的名称相同的函数和模板函数,并且可以只给他们传递一个参数。然后继续向下筛选,接下来观察类型匹配,#1和#2可以进行类型转换以适合参数,#3、#5和#6都是符合参数类型的,char类型值无法转化为指针类型,故而要排除#4和#7两种情况。
在接下来,要匹配最佳的可行函数。通常情况下,从最差到最佳的顺序如下:
- 完全匹配,但常规函数优于模板
- 提升转换,如
char
和shorts
自动转化为int
,float
自动转化为double
- 标准转换,如
int
转换为char
,long
转换为double
- 用户定义的转换,如类声明中定义的转换
由于#1和#2进行了类型转换,所以这两个要劣于#3、#5和#6,单独对于#1和#2来讲,#1要优于#2,因为#1为提升转换,而#2为标准转换。然后排除#1和#2。之后再看模板与常规函数,常规函数要优于模板,因此排除#6,剩下来了#3和#5。那么剩下的这两个怎么办呢?这就引出了什么是完全匹配?如果两个函数都完全匹配(#3和#5)那么怎么办呢?
通常来讲,有两个函数完全匹配是一种错误,但是这一规则有两个例外。
最佳匹配与完全匹配
进行完全匹配是,C++允许某些无关紧要的匹配 如下:
从实参 | 到形参 |
---|---|
T | T & |
T& | T |
T[] | * T |
T(参数列表) | T(*)(参数列表) |
T | const T |
T | volatile T |
T* | const T |
T* | volatile T* |
如果有多个匹配的原型,编译器无法完成重载解析工作,没有最佳的函数,那么编译器将报错。然而,有时候,即使两个函数都完全匹配,仍可以完成函数解析。
-
指向非const数据的指针和引用优先与非const指针和引用匹配
-
const与非const之间的区别只适用于指针和引用指向的数据
-
一个完全匹配优于另一个的情况是,其中一个是非模板函数,它优于模板函数
-
如果完全匹配的两个都是模板函数,那么较为具体的模板函数优先,例如显式具体化优先于普通模板函数
-
术语“较具体”表示编译器确定使用哪种类型时进行的转换最少
-
template <class T> void f1(T t);//#1 template <class T> void f1(T *t);//#2 f1(&a);//int a;
调用f1()时,对于#1来说T可以转换为
int *
,对于#2来说转化为int
因而#2转换的更少,更具体。
-
-
用于找出最具体模板的规则被称为函数模板的部分排序规则,是C++98新增的特性
常见模板函数调用方式
与常规函数一样,可以把函数模板声明和定义结合到一起,这样,在使用函数前提供模板函数的定义,他也将充当函数原型。
template <class T>
T lesser(T a,T b)
int lesser(int a,int b);
有以下四种函数调用:
lesser(x,y);//double x,y;//调用模板函数(与模板函数匹配)
lesser(m,n);//int m,n;//调用非模板函数(与模板函数与非模板函数均匹配)
lesser<>(m,n);//调用模板函数,<>指出应该调用模板函数而非非模板函数,然后根据实参类型实例化
lesser<int>(m,n);//<int>表明调用模板函数,并且实例化,然后将实参转化为类型int
对于模板函数中不确定所得数据类型时怎么办呢?
在创建函数模板时,我们可能会遇到这种情况:
template <class T>
?type? f1(T b,T c)
{
?type? a=b*c;
return a;
}
a 应该规定为什么数据类型好呢?
函数返回类型又是什么呢?
关键字
decltype
(c++11)
decltype
(c++11)
这个关键字用于推断表达式数据类型eg:
int x;
decltype(x) y;
上面表达式就是将y设置为x的数据类型。可以同时初始化。
实际上的decltype
比上面的表示复杂。
假设有下面的定义:
decltype(expression) var;
为确定类型编译器必须遍历一个核对表,简化版如下:
- 第一步:如果
expression
是一个没有用括号括起来的标识符,那么,var类型与该标识 符类型相同,包括const
等限定符。 - 第二步:如果
expression
是一个函数调用,那么var类型应该与函数返回类型相同。- 这个并不会执行函数,只是编译器查看函数的返回类型,而不会实际调用函数。
- 第三步:当
expression
是用括号括起来的标识符时,进入第三步,如果expression
是一个左值,那么var为指向其类型的引用。- 括号并不改变表达式的值和左值性。
- 第四步:如果前面的类型都不满足,那么var与
expression
类型相同。
ps:
int b,d;int &a=b;int &c=d;
虽然a和c都是引用,但是a+c不是引用,他为两个值的和,为int
类型。
如果需要多次声明可以结合typedef
使用:
typedef decltype(x*y) TT;
TT z;
TT m=z;
......
后置返回类型
用于解决无法确定函数模板函数返回类型的情况。
我们学习完decltype
很容易可以想到可不可以在函数声明时把返回类型设置为decltype(x*y)
呢?
答案是不能够的,x和y在函数参数列表中,此时还未声明,所以这两个参数是不存在的。
为此,C++新增了一种声明和语法:
double f1(int,float);
auto f1(int float)->double;
上面两种声明是等价的,将返回类型移到了参数声明后面,->double
成为后置返回类型。其中,auto
是一个占位符,表示后置返回类型提供的类型,这是C++给auto新增加的一个功能,这种语法同样适用于函数定义式声明和定义。
由此,我们可以结合后置返回类型和关键字decltype
来确认模板函数返回类型:
template <class T1,class T2>
auto f1(T1 x,T2 y)->decltype(x*y)
{
return x*y;
}