引子
昨天在某个群看到了这样一段代码:
#include <iostream> #include <string> using namespace std; void foo(string str) { cout << "string" << endl; } void foo(bool b) { cout << "bool" << endl; } void foo(char c) { cout << "char" << endl; } int main() { foo("hello, world"); return 0; }
现在的问题是,这段程序的输出是什么?或者说,foo("hello, world");
调用的究竟是哪个函数?
要解决这个问题,我们就要详细讨论一下函数重载的规则。
注释:
- 以下内容均在 C++11 标准下讨论。
- 暂时先不讨论模板,列表初始化等特性。
函数?
在解决函数重载的问题前,我们先讨论下函数本身。
简单来说,一个函数需要包含如下三个要素:
- 函数名;
- 形参列表(这个函数能接纳几个参数);
- 返回值类型。
而我们常说的函数重载,就是允许两个拥有相同函数名的函数,在形参列表上不同。
因为返回值类型与本文要讨论的主题(函数重载)关系并不密切,因此我们接下来重点讨论形参列表的那些事。
成员函数与非成员函数
首先,根据一个函数是否属于一个类,我们将函数分为成员函数和非成员函数。
而非静态成员函数的形参列表,会在开头多一个指向当前对象的指针作为第一个形参。
(静态成员函数的形参列表也会在开头多一个形参,不过该形参可以匹配任何类型,这里不作深入讨论)
默认参数
有些参数可以具有默认值,为了避免歧义,这些具有默认值的参数必须置于形参列表的最末尾。
#include <iostream> using namespace std; int dist(int x1, int y1, int x2 = 0, int y2 = 0) { return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); } int main() { cout << dist(6, 5) << endl; // 等价于 dist(6,5,0,0) return 0; }
变长实参
在 scanf
和 printf
中,除了第一个参数是确定的 const char*
类型外,剩下的参数的数量,每个参数的类型,都是不确定的。
在 C 语言的时代,这个问题该怎么解决?
答案是使用变长实参。
int scanf( const char* format, ... );
这就是 scanf
的函数声明。
C 中要求省略号前必须有一个具名形参,而 C++ 中无此要求(事实上如果没有具名形参的话,将无法访问传递给这种函数的实参)。
因为变长实参的相关实现细节与本文主题没有太大关联,这里不再展开,感兴趣的读者可以参考 可变参数入门 – wenge 的博客。
可行函数
当我们要调用函数 f
时,相同名字的函数定义可能会非常多,这里面哪些函数是符合要求的呢?
先考虑形参和实参的个数。我们设形参有 $N$ 个,实参有 $M$ 个。
- 若 \(N=M\),则符合要求。
- 若 \(N<M\),且存在一个省略号形参,则符合要求。
- 若 \(N>M\),且自第 \(M+1\) 个形参起均有默认值,则符合要求。
接下来考虑实参和形参间的类型转换。对于每个实参,必须存在一个隐式转换序列,能将其转换为对应的形参类型。
最后考虑引用类型。右值实参不能对应一个左值非 const
引用形参,而左值实参也不能对应一个右值引用的形参。
#include <iostream> using namespace std; void fun(string &str) { cout << "string&" << endl; } void fun(string str) { cout << "string" << endl; } int main() { fun("Hello, world!"); // 这里传入一个右值实参,而第一个函数形参为左值非 const 引用,故第一个函数不可行 return 0; }
最佳函数
选出了可行函数之后,如果只有一个可行函数,调用这个函数就行了。
如果有多个呢?我们需要从中找到一个最贴近的函数声明。
简单来说,对于两个可行函数 f1
和 f2
,我们需将第 \(i\) 实参和第 \(i\) 形参之间的隐式转换序列进行比较。f1
优于 f2
,当且仅当 f1
的所有实参的隐式转换序列均不劣于 f2
,且存在一个 f1
实参的隐式转换序列优于 f2
相应实参的转换序列。
将所有函数比较过后,若存在一个函数优于其他所有函数,则最终将调用该函数,否则将找不到合适的函数调用,编译失败。
隐式转换序列比较
简单来说,隐式转换序列可以分为三类:标准转换序列,用户定义转换序列,省略号转换序列。
标准转换序列由下列部分按顺序构成:
- 下列三者中的最多一个:左值转右值,数组转指针,函数转指针。
- 下列两者中的最多一个:数值提升,数值转换。
- 最多一个限定转换。
而用户定义转换序列,简单来说就是利用构造函数完成转换。
例如之所以能向一个接受 std::string
的函数传入一个类型为 const char*
的变量,原因在于存在构造函数 std::string(const CharT* s,const Allocator& alloc = Allocator());
。
省略号转换序列,自然是利用省略号形参完成的转换。
C++ 标准指出,在比较各实参-形参的转换序列时,标准转换序列总是优于用户定义的转换序列,而用户定义的转换序列总是优于省略号转换序列。
到这里开头的问题就能够很好解释了。虽然 std::string
看上去与实参类型更像,但 const char*
到 std::string
的转换属于用户定义的转换序列(由构造函数定义转换规则)。
而 const char*
到 bool
的转换属于标准转换序列(可以通过数值转换将指针转换为布尔值)。
因此经过比较,调用形参类型为 bool
的函数更优。
如果两个序列都是标准转换序列呢?这时候我们需要比较两个标准转换序列的等级。
我们将转换操作分为三个等级:
- 完全一致:不转换,左值转右值,限定性转换。
- 提升:整型提升,浮点提升。
- 转换:整型转换,浮点转换,浮点转整型,指针转换,布尔转换。
一个转换序列的等级,等于其最劣操作的等级。
接下来我们就可以讨论两个标准转换序列 S1 和 S2 谁更优了:
- 若 S1 是 S2 的子序列,则 S1 更优(S1 与 S2 相比少了冗余操作)。
- 否则,若 S1 等级优于 S2,则 S1 更优。
- 否则,若存在一个引用形参,则右值到右值引用的转换优于左值到右值引用的转换。
- 否则,比较两个引用形参被
const
限定的数目,const
限定少的更优。
对于上面比较规则的 3 和 4,下面给出两个例子(来自 cppreference)
int i; int f1(); int g(const int&); int g(const int&&); int j = g(i); // 传入左值,只能调用 g(const int&) int k = g(f1()); // 传入右值,因为第一个函数的左值引用有 const 限定,故两者皆可行 // 但转换为右值引用更优,故调用 g(const int&&)
int f(const int &); int f(int &); int g(const int &); int g(int); int i; int j = f(i); // 两个 f 函数均接受引用,但 f(int&) 少一个 const 限定,因而更优 int k = g(i); // 左值 i -> const int& 排行为准确匹配 // 左值 i -> 右值 int 排行为准确匹配 // 两者等级相同,无法比出最优函数 // 无法编译
后记
事实上函数重载需要考虑的情况还有不少(比如 C++11 中的列表初始化等新特性),但是因为上面的内容已经足够解释开头的问题了,就先写到这里吧。
剩下的之后看心情会继续填坑。