函数匹配
在大多数情况下,我们容易确定某次调用应该选用哪个重载函数。
然而,当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时,这项工作就不那么容易了。
以下面这组函数及其调用为例:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // 调用 void f(double, double)
确定候选函数和可行函数
候选函数
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。
候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
在这个例子中,有4个名为f的候选函数。
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
可行函数
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
可行函数也有两个特征:
- 一是其形参数量与本次调用提供的实参数量相等,
- 二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
我们能根据实参的数量从候选函数中排除掉两个。
不使用形参的函数和使用两个int形参的函数显然都不适合本次调用,这是因为我们的调用只提供了一个实参,而它们分别有0个和两个形参。
使用一个int 形参的函数和使用两个double形参的函数是可行的,它们都能用一个实参调用,其中最后那个函数本应该接受两个double值,但能因为它各有一个默认实参,所以只用一个实参也能调用它。
如果函教含有默认实参,则我们在调用该画就明体Note 入的实参数量可能少于它实际使用的实参数量。
在使用实参数量初步判别了候选函数后,接下来考察实参的类型是否与形参匹配。
和一般的函数调用类似,实参与形参匹配的含义可能是它们具有相同的类型,也可能是实参和形参类型满足转换规则。
在上面的例子中,剩下的两个函数都是可行的:
- f(int)是可行的,因为实参类型double能转换成形参类型int。
- f(double, double)是可行的,因为它的第三个形参提供了默认值,而第一个形参的类型正好是double,与函数使用的实参类型完全一致。
可行函数就只有
void f(int);
void f(double, double = 3.14);
如果没找到可行函数,编译器将报告无匹配函数的错误。
寻找最佳匹配(如果有的话)
函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。
在这一过程中,逐检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。
下一节将介绍“最匹配”的细节,它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。
在我们的例子中,调用只提供了一个(显式的)实参,它的类型是double。
如果调用f(int),实参将不得不从double转换成int。
另一个可行函数f(double,double)则与实参精确匹配。
精确匹配比需要类型转换的匹配更好,因此,编译器把f(5.6)解析成对含有两个double形参的函数的调用,并使用默认值填补我们未提供的第二个实参。
我们可以验证一下
#include<iostream>
using namespace std;
void f(){}
void f(int){}
void f(int, int){}
void f(double, double = 3.14) { cout << "调用f(double,double)版本" << endl; }
int main()
{
f(5.6); // 调用 void f(double, double)
}
含有多个形参的函数匹配
当实参的数量有两个或更多时,函数匹配就比较复杂了。
对于前面那些名为f的函数,我们来分析如下的调用会发生什么情况:
f(42,2.56);
选择可行函数的方法和只有一个实参时一样,编译器选择那些形参数量满足要求且实参类型和形参类型能够匹配的函数。
此例中,可行函数包括f(int,int)和f(double,double)。
接下来,编译器依次检查每个实参以确定哪个函数是最佳匹配。
如果有且只有一个函数满足下列条件,则匹配成功:
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
- 至少有一个实参的匹配优于其他可行函数提供的匹配。
如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。
在上面的调用中,只考虑第一个实参时我们发现函数f(int,int)能精确匹配。要想匹配第二个函数,int 类型的实参必须转换成double类型。显然需要内置类型转换配劣于精确匹配,因此仅就第一个实参来说,f(int,int)比f(double,double)更好。
接着考虑第二个实参2.56时,f(double,double)是精确匹配;要想调用f(int,int)必须将2.56从double类型转换成int类型。因此仅就第二个实参来说,f(double,double)更好。
编译器最终将因为这个调用具有三义性而拒绝其请求:因为每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断哪个更好。
看起来我们似乎可以通过强制类型转换其中的一个实参来实现函数的匹配,但是在设计良好的系统中,不应该对实参进行强制类型转换。
调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
1. 精确匹配,包括以下情况:
- 实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型。
- 向实参添加顶层const或者从实参中删除顶层const。
2. 通过const转换实现的匹配。
3.通过类型提升实现的匹配。
4.通过算术类型转换或指针转换实现的匹配。
5.通过类类型转换实现的匹配。
需要类型提升和算术类型转换的匹配
内置类型的提升和转换可能在函数匹配时产生意想不到的结果,但幸运的是,在设计良好的系统中函数很少会含有与下面例子类似的形参。
分析函数调用前,我们应该知道小整型一般都会提升到int类型或更大的整数类型。
假设有两个函数,一个int、另一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。有时候,即使实参是一个很小的整数值,也会直接将它提升成int类型:此时使用short版本反而会导致类型转换:
void ff(int);
void ff(short);
ff('a');// char 提升成int;调用 f(int)
所有算术类型转换的级别都一样。例如,从int 向unsigned int的转换并不比int向double的转换级别高。
举个具体点的例子,考虑
void manip(long);
void manip(float);
manip(3,14); //错误:二义性调用
字面值3.14的类型是double,它既能转换成long也能转换成float。因为存在两种可能的算数类型转换,所以该调用具有二义性。
函数匹配和const实参
如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数:
#include<iostream>
using namespace std;
void lookup(int&) {
cout << "调用int&版本" << endl;
}
void lookup(const int&) {
cout << "调用const int&版本" << endl;
}
//调用1ookup (Account&)
int main()
{
const int a = 0;
int b;
lookup(a);
lookup(b);// 调用 lookup(const Account&)
}
在第一个调用中,我们传入的是const对象a。
因为不能把普通引用绑定到const对象上,所以此例中唯一可行的函数是以常量引用作为形参的那个函数,并且调用该函数与实参a精确匹配
在第二个调用中,我们传入的是非常量对象b。
对于这个调用来说,两个函数都是可行的,因为我们既可以使用b初始化常量引用也可以用它初始化非常量引用。然而,用非常量对象初始化常量引用需要类型转换,接受非常量形参的版本则与b精确匹配。因此应该选用非常量版本的函数。
指针类型的形参也类似。如果两个函数的唯一区别是它的指针形参指向常量或非常量,则编译器能通过实参是否是常量决定选用哪个函数:
如果实参是指向常量的指针,调用实参是const*的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。