专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!
目录
- 6.6函数匹配
- 确定候选函数和可行函数
- 寻找最佳匹配(如果有的话)
- 含有多个形参的函数匹配
- 实参类型转换
- 函数匹配和const实参
6.6函数匹配
在大多数情况下,我们容易确定某次调用应该选用哪个重载函数。然而,当几个重载函数的形参数量相等以及树些形参的类型可以由其他类型转换得来时,这项工作就不那么容易了。以下面这组函数及其调用为例:
void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6);//调用void(double,double)
确定候选函数和可行函数
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:一是与被调用的函数合名,二是其声明在调用点可见。在这个例子中,有4个名为f的候选函数。
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(variable function)。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
我们能根据实参的数量从候选函数中排除掉两个。不使用形参的函数和使用两个int形参的函数显然都不适合本次调用,这是因为我们的调用只提供了一个实参,而它们分别有0个和两个形参。
使用一个int形参的函数和使用两个double形参的函数是可行的,它们都能用一个实参调用。其中最后那个函数本应该接受两个double值,但是因为它含有一个默认实
参,所以只用一个实参也能调用它。
在使用实参数量初步判别了候选函数后,接下来考察实参的类型是否与形参匹配。和一般的函数调用类似,实参与形参匹配的含义可能是它们具有相同的类型,也可能是实参类型和形参类型满足转换规则。在上面的例子中,剩下的两个函数都是可行的:
- f(int)是可行的,因为实参类型double能转换成形参类型int。
- f(double,double)是可行的,因为它的第二个形参提供了默认值,而第一个形参的类型正好是double,与函数使用的实参类型完全一致。
如果没找到可行函数,编译器将报告无匹配函数的错误。
寻找最佳匹配(如果有的话)
函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。下一节将
介绍“最匹配“的细节,它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。
在我们的例子中,调用只提供了一个(显式的实参,它的类型是double。如果调用f(int),实参将不得不从double转换成int。另一个可行函数f(double,double)则与实参精确匹配。精确匹配比需要类型转换的匹配更好,因此,编译器把f(5.6)解析成对含有两个double形参的函数的调用,并使用默认值填补我们未提供的第二个实参。
含有多个形参的函数匹配
当实参的数量有两个或更多时,函数匹配就比较复杂了。对于前面那些名为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)更好。
编译器最终将因为这个调用具有二义性而拒绝其请求:因为每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断孰优孰劣。看起来我们似乎可以通过强制类型转换其中的一个实参来实现函数的匹配,但是在设计良好的系统中,不应该对实参进行强制类型转换。
调用重载函数时应尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理:
实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成儿个等级,具体排序如下所示:
- 精确匹配,包括以下情况:
- 实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型.
- 向实参添加顶层const或者从实参中删除顶层const。
- 通过const转换实现的匹配。
- 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配。
需要类型提升和算术类型转换的匹配
内置类型的提升和转换可能在函数匹配时产生意想不到的结栗,但幸运的是,在设计良好的系统中函数很少会含有与下面例子类似的形参。
分析函数调用前,我们应该知道小整型一般都会提升到int类型或更大的整数类型。
假设有两个函数,一个接受int、另一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。有时候,即使实参是一个很小的整数值,也会直接将它提升成int类型;此时使用short版本反而会导致类型转换:
void ff(int);
void ff(short);
ff(7)//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,则当调用发生时编详器通过实参是否是常量来决定选择哪个函数:
Record lookup(Account&);//函数的参数是Account的引用
Record lookup(const Account&);// 函数的参数是一个常量引用
const Account a;
Account b;
lookup(a);//调用lookup(const Account&)
lookup(b);//调用lookup(Account&)
在第一个调用中,我们传入的是const对象a,因为不能把普通引用绑定到const对象上,所以此例中唯一可行的函数是以常量引用作为形参的那个函数,并且调用该函数与实参a精确匹配。
在第二个调用中,我们传入的是非常量对象b。对于这个调用米说,两个函数都是可行的,因为我们既可以使用b初始化常量引用也可以用它初始化非常量引用。然而,用非常量对象初始化常量引用需要类型转换,接受非常量形参的版本则与b精确匹配。因此,应该选用非常量版本的函数。
指针类型的形参也类似。如果两个函数的唯一区别是它的指针形参指向常量或非常量,则编译器能通过实参是否是常量决定选用哪个函数,如果实参是指向常量的指针,调用形参是const*的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。