在上节课(函数重载区分)中,我们讨论了编译器如何凭借参数个数、类型等属性来区分同名函数。若未能正确区分,编译器会立即报错。
然而,仅有“可区分的重载集合”只是第一步。当真正发生一次函数调用时,编译器还必须确保能够找到并匹配唯一的函数声明。
- 对于非重载函数(唯一名称),要么成功匹配,要么无可匹配并报错。
- 对于重载函数,可能存在多个候选版本。编译器必须从中选出“最佳”匹配,这一过程称为重载决议(overload resolution)。
在参数类型与形参类型完全吻合的简单场景下,决议通常直接明了:
#include <iostream>
void print(int x) { std::cout << x << '\n'; }
void print(double d){ std::cout << d << '\n'; }
int main()
{
print(5); // 5 是 int,匹配 print(int)
print(6.7); // 6.7 是 double,匹配 print(double)
return 0;
}
但若调用时的实参类型与任何重载版本的形参都不完全一致,该怎么办?例如:
#include <iostream>
void print(int x) { std::cout << x << '\n'; }
void print(double d){ std::cout << d << '\n'; }
int main()
{
print('a'); // char 既非 int 也非 double
print(5L); // long 既非 int 也非 double
return 0;
}
显然,没有精确匹配,但 char
和 long
都能隐式转换为 int
或 double
。那么,哪一次转换才是“最佳”?本课将完整说明编译器的匹配流程。
重载决议的步骤概览
当调用重载函数时,编译器按以下六步顺序逐一尝试匹配;每一步都会应用若干类型转换,并检查转换后是否出现匹配。每步只能出现三种结果:
- 无匹配:继续下一步;
- 唯⼀匹配:该函数即为最佳匹配,过程结束;
- 多个匹配:产生二义性(ambiguous match)编译错误。
若六步结束仍未匹配,最终报“无匹配函数”错误。
参数匹配序列
步骤 1:精确匹配
分两阶段:
- 类型完全吻合:实参与形参类型完全一致。
- 平凡转换(trivial conversions):
- 左值转右值
- 限定符转换(如非 const → const)
- 非引用转引用
示例:
void foo(int) {}
void foo(const int&) {}
int main()
{
int x{1};
foo(x); // 平凡转换 int → const int&,也视为精确匹配
}
若多组平凡转换后仍出现多个精确匹配,则报二义性:
void foo(int) {}
void foo(const int&) {}
int main(){ int x{1}; foo(x); } // 二义性
步骤 2:数值提升匹配
若步骤 1 无匹配,编译器尝试数值提升(numeric promotion)。
void foo(int) {}
void foo(double){}
int main()
{
foo('a'); // char → int(提升),匹配 foo(int)
foo(4.5f); // float → double(提升),匹配 foo(double)
}
提升匹配优于后续任何数值转换。
步骤 3:数值转换匹配
无提升匹配时,再尝试数值转换(numeric conversion)。
#include <string>
void foo(double) {}
void foo(std::string) {}
int main()
{
foo('a'); // char → double(数值转换),匹配 foo(double)
}
步骤 4:用户自定义转换
若仍未匹配,编译器查找用户自定义转换(如类类型定义的类型转换运算符或构造函数)。示例(仅示意,类细节稍后详述):
class X
{
public:
operator int() { return 0; } // X → int 的用户自定义转换
};
void foo(int) {}
void foo(double){}
int main()
{
X x;
foo(x); // 先找不到精确、提升、数值转换,最终通过 X→int 匹配 foo(int)
}
相关内容 见 21.11 课 —— 重载类型转换运算符。
步骤 5:可变参数匹配
若仍无匹配,编译器检查是否有使用可变参数(ellipsis)的函数。
相关内容 见 20.5 课 —— 可变参数及其避免方法。
步骤 6:无可匹配函数
若以上五步皆未果,报编译错误。
二义性匹配
非重载场景下,调用要么成功,要么无可匹配直接报错;重载场景下则可能出现第三种结果:二义性。
示例 1:
void foo(int) {}
void foo(double) {}
int main()
{
foo(5L); // long 可转为 int 或 double,均通过数值转换 → 二义性
}
VS2019 报错:
error C2668: 'foo': ambiguous call to overloaded function
示例 2:
void foo(unsigned int) {}
void foo(float) {}
int main()
{
foo(0); // int → unsigned int 或 float,均数值转换 → 二义性
foo(3.14159); // double → unsigned int 或 float,均数值转换 → 二义性
}
供进阶读者 默认实参亦可能导致二义性,见 11.5 课 —— 默认实参。
解决二义性
编译期必须消除二义性,常用方法:
- 新增重载版本,提供精确匹配类型;
- 显式强制转换实参;
- 使用字面量后缀指定类型。
示例:
int x{0};
foo(static_cast<unsigned int>(x)); // 明确调用 foo(unsigned int)
foo(0u); // 字面量后缀,精确匹配
多参数决议
若函数有多个形参,编译器对每个实参依次应用规则,并寻找整体最优函数:
- 每个实参至少与其他候选函数匹配得“一样好”;
- 至少有一个实参比其他候选函数匹配得“更好”。
示例:
#include <iostream>
void print(char, int) { std::cout << 'a' << '\n'; }
void print(char, double){ std::cout << 'b' << '\n'; }
void print(char, float) { std::cout << 'c' << '\n'; }
int main()
{
print('x', 'a'); // 第一个实参精确匹配 char;第二个实参 char→int 提升优于其他转换 → 选择 print(char,int)
}
若找不到满足上述条件的函数,则调用被判定为二义性或无可匹配。