Reference:
- 《C++ Primer. 4th ed》
What's overloading?
函数的重载也就是用一个函数名代表多个不同的函数。这些不同的函数之间需要用不同的参数列表来区分。单单是返回值类型的不同不能够构成重载,而只是一个编译错误。而如果两个函数声明完全一样的话,则称为Redeclaration(重声明)。
所以
Record lookup(const Accout&);
bool lookup(const Accout&);
是编译错误,而
Record lookup(const Accout&);
Record lookup(const Accout&);
是重声明。
但是在重载的时候,要注意,不是说参数列表有不一样的,就是重载。要注意以下的区别:
- 参数名的有无不作为区别的标准。
- 参数类型如果是typedef的话,不作为区分的标准。
- 默认参数的有无不作为区分的标准。
- 对于非引用和非指针类型,const声明不作为区分的标准。
对于1,也就是如下是相同的:
Record lookup(const A& a);
Record lookup(const A& );
对于2,如下:
typedef A B;
Record lookup(const A&);
Record lookup(const B&);
对于3,如下:
Record lookup(const int i = 1);
Record lookup(const int i);
对于4,如下:
Record lookup(const A);
Record lookup(A);
Overloading and Scope rules
如果我们在global scope定义了两个f函数,而在函数内部作用域又声明了一个f函数,那么结果又是怎么样呢?代码如下:
void f(int);
void f(double);
void b()
{
void f(long);
f(1);
}
在这里,scoping的作用发生于overload resolution的作用之前。
也就是说,编译器是这样解决function overload的:
- 先往上找到一个scope,这个scope里面包含着f(可能有overload)的声明。然后不再往上找。
- 然后进行一般的function overloading resolution(会在下一节讲解)。
所以在上面的例子中,在函数调用f(1)的时候,编译器首先在local scope找到了void f(long);,而这个是这个scope里面唯一的f函数声明,所以后面的overload resolution就只会在这个f(long)上做文章。而global scope则被void f(long);隐藏了。
Function Overloading resolution
当我们有了好几个Overload的函数之后,是怎么从中选择最终的一个函数作为调用的函数呢?这个筛选的过程就是Overloading resolution。
C++中的overloading resolution分为以下几个过程:
- 选择Candidate functions
- 从candidates functions中选出viable functions
- 从viable functions里选择出最好的一个match,作为作用调用的函数。
Select Candidate Functions
假设我们现在在代码里面有一函数调用的代码如下:
void f(int);
void f(double);
void b()
{
void f(int);
void f(int, int)
void f(double, double);
void f(double, double d = 1.0);
f(1);
}
所谓的Candidate Functions,就是说在函数调用的作用域里面可见的同名函数。
所以在上面的代码中,全局的两个函数f是不算做candidate。而函数b里面的所有f声明都是f函数的candidates。
Select Viable Functions
接下来,编译器会从candidate functions里面选择一些更加符合要求的函数作为viable functions。
Viable Functions必须满足下面两个条件:
- 首先,函数必须的函数调用有相同的参数个数。
- 其次,函数调用的参数必须和函数声明的参数类型完全吻合或者可以转化成对应的类型。
基于上面的条件,我们可以从例子中看到,来到第二步,首先参数个数里面void f(int, int)和void f(double, double)都是参数个数不对,所以都不是viable function。
而void f(int); 来说,因为1就是int类型的,所以这个类型是匹配的,所以void f(int)是一个viable function。
而void f(double, double d = 1.0)来说,因为他有一个默认参数,而编译器对于默认参数的处理是在函数调用的时候自动补上,所以f(1)也可能是在实际调用的时候变成f(1, 1.0)。而1也是可以被转换成double的,所以void f(double, double d = 1.0)也是一个viable function。
所以在这一步之后,会有两个viable function。那么接下来就是选择最好的匹配了。
Select Best Match
在viable functions的基础上,编译器会从中挑选出一个最好的match,作为最终的调用函数。
其中最好的一个match具有下面的性质:
- 每个实参和形参的对应都不比其他的viable functions差。原文(The match for each argument is no worse than the match required by any other viable functions)
- 至少有一个实参和形参的对应是比其他的viable function好。原文(There is at least one argument for which the match is better than the match provided by any other viable function.)
如果没有一个函数是best match的话,那么这个函数调用就是有歧义的。
其中在上面的定义中最关键的一点就是一个是实参和形参的match怎么样才算比其他的match要好?
在C++里面,编译器对于类型转换有下面一个rank,rank从高到低:
- 精确匹配,也就是说实参跟形参的类型一致。
- 实参通过promotion转化成形参的类型。
- 实参通过标准转型而转化成形参的类型。
- 实参通过类定义的转型而转化成形参的类型。
第一条应该最好理解。
对于第二条,在C++里面,promotion涉及两种类型,整数型和浮点型。整数型的类型,实参如果是小于int的类型的话,而形参是int的话,那么这个实参就会被转化成int,而不会丢失他的值,这称之为整形promotion。对于浮点型,所有的double以下的类型转化成double也称之为promotion。
对于其他的基本类型转化,C++都认为是标准转化。注意,int转化成long也算是标准转化,而不是promotion。
而对于类的类型转化,就是通过重载operator int()这一类的函数重载或者是通过默认的构造函数来默认构造一个对象。
对于上面的类型,对于f(1)来说,void f(int)的参数是一个精确匹配,而void f(double, double d = 1.0)是一个标准转化,所以best match是void f(int)。
基于以上的原则,编译器就可以找到一个最好的匹配,从而精确的知道在调用函数的那一刻是调用的哪个函数。
Misc
在《C++ Primer》里面,有这么一个提醒:In practice, arguments should not need casts when calling overloaded functions: The need for a cast means that the parameter sets are designed poorly.
所以我们在设计API的时候,一定要记得设计好参数类型,不至于出现歧义的情况。