函数重载决议与二义性匹配

在上节课(函数重载区分)中,我们讨论了编译器如何凭借参数个数、类型等属性来区分同名函数。若未能正确区分,编译器会立即报错。

然而,仅有“可区分的重载集合”只是第一步。当真正发生一次函数调用时,编译器还必须确保能够找到并匹配唯一的函数声明。

  • 对于非重载函数(唯一名称),要么成功匹配,要么无可匹配并报错。
  • 对于重载函数,可能存在多个候选版本。编译器必须从中选出“最佳”匹配,这一过程称为重载决议(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;
}

显然,没有精确匹配,但 charlong 都能隐式转换为 intdouble。那么,哪一次转换才是“最佳”?本课将完整说明编译器的匹配流程。

重载决议的步骤概览

当调用重载函数时,编译器按以下六步顺序逐一尝试匹配;每一步都会应用若干类型转换,并检查转换后是否出现匹配。每步只能出现三种结果:

  1. 无匹配:继续下一步;
  2. 唯⼀匹配:该函数即为最佳匹配,过程结束;
  3. 多个匹配:产生二义性(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 课 —— 默认实参。

解决二义性

编译期必须消除二义性,常用方法:

  1. 新增重载版本,提供精确匹配类型;
  2. 显式强制转换实参;
  3. 使用字面量后缀指定类型。

示例:

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)
}

若找不到满足上述条件的函数,则调用被判定为二义性或无可匹配。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"