11-3 函数重载解析与模糊匹配
在上一课(11.2——函数重载区分)中,我们探讨了函数的哪些属性可用于区分重载函数。若重载函数未能与同名其他重载版本正确区分,编译器将报错。
然而,拥有可区分的重载函数集仅是基础。每次函数调用时,编译器还必须确保能找到匹配的函数声明。
对于非重载函数(即具有唯一名称的函数),每次函数调用仅可能匹配一个函数。该函数要么完全匹配(或经类型转换后可匹配),要么完全不匹配(导致编译错误)。而重载函数可能存在多个潜在匹配项。由于函数调用只能解析为其中一个,编译器必须确定最佳匹配的重载函数。将函数调用与特定重载函数进行匹配的过程称为重载解析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 is an int, so this matches print(int)
print(6.7); // 6.7 is a double, so this matches 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 does not match int or double, so what happens?
print(5L); // long does not match int or double, so what happens?
return 0;
}

即使存在不完全匹配,也并非无法找到匹配项——毕竟char或long类型可隐式转换为int或double。但每种情况下哪种转换最优?
本节将探讨编译器如何将函数调用与特定重载函数进行匹配。
重载函数调用的解析
当调用重载函数时,编译器会依次执行一系列规则来确定哪个重载函数(若存在)最匹配(具体步骤将在下文详述)。
在每个步骤中,编译器会对函数调用中的实参应用多种类型转换。每次转换后,编译器都会检查是否存在匹配的重载函数。当所有类型转换及匹配检查完成后,该步骤即告结束。最终结果可能有三种:
- 未找到匹配函数。编译器进入序列中的下一步。
- 仅找到一个匹配函数。该函数被视为最佳匹配,匹配过程即告完成,后续步骤不再执行。
- 找到多个匹配函数。编译器将报出歧义匹配编译错误,我们稍后将详细讨论此情况。
如果编译器遍历整个序列后仍未找到匹配项,则会生成编译错误,提示无法为该函数调用找到匹配的重载函数。
实参匹配序列
步骤 1) 编译器尝试寻找精确匹配。此过程分为两个阶段:首先检查是否存在重载函数,其函数调用中的实参类型与重载函数的形参类型完全匹配。例如:
void foo(int)
{
}
void foo(double)
{
}
int main()
{
foo(0); // exact match with foo(int)
foo(3.4); // exact match with foo(double)
return 0;
}
由于函数调用 foo(0) 中的 0 是 int 类型,编译器将检查是否声明了 foo(int) 重载。由于存在该重载,编译器判定 foo(int) 为精确匹配。
其次,编译器会对函数调用中的实参应用若干简单转换。这些简单转换trivial conversions旨在通过修改类型(不改变值)来寻找匹配项,具体包括:
- 左值转右值转换
- 限定转换(如非const转const)
- 非引用转引用
例如:
void foo(const int)
{
}
void foo(const double&) // double& is a reference to a double
{
}
int main()
{
int x { 1 };
foo(x); // x trivially converted from int to const int
double d { 2.3 };
foo(d); // d trivially converted from double to const double& (non-ref to ref conversion)
return 0;
}
在上例中,我们调用了 foo(x),其中 x 是 int 类型。编译器会将 x 从 int 类型简单转换为 const int 类型,从而匹配 foo(const int)。我们还调用了 foo(d),其中 d 是 double 类型。编译器会将 d 从 double 类型简单转换为 const double& 类型,从而匹配 foo(const double&)。
相关内容
我们在第12.3节——左值引用中介绍了引用。
通过简单转换匹配的结果被视为精确匹配。这意味着以下程序会导致模糊匹配:
void foo(int)
{
}
void foo(const int&) // int& is a reference to a int
{
}
int main()
{
int x { 1 };
foo(x); // ambiguous match with foo(int) and foo(const int&)
return 0;
}
步骤 2) 若未找到完全匹配,编译器将尝试通过对参数进行数值提升来寻找匹配项。在第 10.1 节——隐式类型转换中,我们已探讨过某些窄整型和浮点型如何能自动提升为更宽的类型(如 int 或 double)。若数值提升后找到匹配项,则函数调用解析完成。
例如:
void foo(int)
{
}
void foo(double)
{
}
int main()
{
foo('a'); // promoted to match foo(int)
foo(true); // promoted to match foo(int)
foo(4.5f); // promoted to match foo(double)
return 0;
}
对于 foo(‘a’),由于在上一步中未能找到与 foo(char) 完全匹配的函数,编译器将字符 ‘a’ 提升为整型,并继续寻找匹配项。此时匹配到 foo(int),因此函数调用最终解析为 foo(int)。
步骤 3) 如果通过数值提升未找到匹配项,编译器将尝试对参数应用数值转换(参见 10.3 ——数值转换)来寻找匹配项。
例如:
#include <string> // for std::string
void foo(double)
{
}
void foo(std::string)
{
}
int main()
{
foo('a'); // 'a' converted to match foo(double)
return 0;
}
在此情况下,由于不存在 foo(char)(精确匹配)和 foo(int)(提升匹配),字符 ‘a’ 被数值转换为 double 类型,并与 foo(double) 实现匹配。
关键要点
通过数值提升实现的匹配优先于通过数值转换实现的匹配。
步骤4) 若数值转换未能找到匹配项,编译器将尝试通过用户定义转换进行匹配。虽然我们尚未讲解用户定义转换,但某些类型(如类)可定义隐式调用的其他类型转换。以下示例仅为说明此原理:
// We haven't covered classes yet, so don't worry if this doesn't make sense
class X // this defines a new type called X
{
public:
operator int() { return 0; } // Here's a user-defined conversion from X to int
};
void foo(int)
{
}
void foo(double)
{
}
int main()
{
X x; // Here, we're creating an object of type X (named x)
foo(x); // x is converted to type int using the user-defined conversion from X to int
return 0;
}
在此示例中,编译器首先会检查是否存在与 foo(X) 完全匹配的定义。我们尚未定义此类匹配。接下来编译器会检查 x 是否可进行数值提升,但它无法提升。随后编译器会检查 x 是否可进行数值转换,但同样无法转换。最后,编译器将查找用户定义的转换。由于我们定义了从 X 到 int 的用户定义转换,编译器将把 X 转换为 int 以匹配 foo(int)。
应用用户定义转换后,编译器可能会进行额外的隐式提升或转换以找到匹配项。因此,如果我们的用户定义转换对象是 char 类型而非 int 类型,编译器将先使用用户定义的 char 转换,再将结果提升为 int 以实现匹配。
相关内容
我们在第21.11节——重载类型转换中讨论了如何为类类型创建用户定义的转换(通过重载类型转换运算符)。
对于进阶读者
类的构造函数也充当从其他类型到该类类型的用户定义转换,可在此步骤中用于查找匹配函数。
步骤 5) 若通过用户定义的转换未找到匹配项,编译器将寻找使用省略号的匹配函数。
相关内容
我们在第 20.5 课——省略号中讲解了省略号(以及为何应避免使用它们)。
步骤 6) 若此时仍未找到匹配项,编译器将放弃搜索并报出无法找到匹配函数的编译错误。
模糊匹配
对于非重载函数,每次函数调用要么解析为具体函数,要么因未找到匹配项而导致编译器报错:
void foo()
{
}
int main()
{
foo(); // okay: match found
goo(); // compile error: no match found
return 0;
}
在重载函数中,还存在第三种可能的结果:可能出现模糊匹配ambiguous match。当编译器在同一步骤中发现两个或多个可匹配的函数时,就会发生模糊匹配。此时编译器将停止匹配,并报出编译错误,指出发现模糊函数调用。
由于每个重载函数都必须被区分才能编译,您可能会疑惑:函数调用如何可能导致多个匹配结果?让我们通过一个示例来阐明:
void foo(int)
{
}
void foo(double)
{
}
int main()
{
foo(5L); // 5L is type long
return 0;
}
由于字面量 5L 的类型为 long,编译器首先会尝试查找 foo(long) 的精确匹配,但不会找到。接下来,编译器会尝试数值提升,但 long 类型的值无法提升,因此这里也没有匹配项。
随后,编译器将尝试通过对 long 实参应用数值转换来寻找匹配项。在检查所有数值转换规则的过程中,编译器将发现两个潜在匹配项:若长整型实参转换为整型,则函数调用将匹配 foo(int);若转换为双精度型,则匹配 foo(double)。由于通过数值转换存在两种可能的匹配,该函数调用被判定为歧义调用。
在 clang21 中,这将导致以下错误信息:

关键要点
如果编译器在某个步骤中找到多个匹配项,则会导致函数调用歧义。这意味着该步骤中的任何匹配项都不会被视为优于同一步骤中的其他匹配项。
以下是另一个产生模糊匹配的示例:
void foo(unsigned int)
{
}
void foo(float)
{
}
int main()
{
foo(0); // int can be numerically converted to unsigned int or to float
foo(3.14159); // double can be numerically converted to unsigned int or to float
return 0;
}
虽然你可能预期 0 会解析为 foo(unsigned int),而 3.14159 会解析为 foo(float),但这两种调用都会导致模糊匹配。整型值 0 既可数值转换为无符号整型,也可转换为浮点型,因此两种重载都同样匹配,结果是模糊的函数调用。

同样的情况也适用于 double 类型转换为 float 或 unsigned int 的情形。这两种转换都是数值转换,因此两种重载都同样匹配良好,结果再次导致模糊。
面向进阶读者
默认参数也可能导致模糊匹配。我们在第11.5节——默认实参中探讨了此类情况。
模糊匹配的解析
由于模糊匹配属于编译时错误,必须在程序编译前消除模糊匹配。解决模糊匹配的方法包括:
- 通常最佳方案是直接定义一个新重载函数,其形参类型与调用函数的实参类型完全匹配。这样C++就能为函数调用找到精确匹配。
- 另一种方法是显式将模糊实参强制转换为目标函数的类型。例如,要使 foo(0) 与上例中的 foo(unsigned int) 匹配,可采用以下写法:
int x{ 0 };
foo(static_cast<unsigned int>(x)); // will call foo(unsigned int)
- 如果你的实参是字面量,可以使用字面量后缀确保字面量被解释为正确类型
foo(0u); // will call foo(unsigned int) since 'u' suffix is unsigned int, so this is now an exact match
最常用的后缀列表可在第5.2课——字面量中找到。
多实参函数匹配规则
当存在多个实参时,编译器将依次对每个实参应用匹配规则。最终选定的函数需满足:所有实参的匹配度均不低于其他候选函数,且至少有一个实参的匹配度优于所有其他函数。换言之,所选函数必须至少在某个形参上提供优于所有候选函数的匹配效果,且在其余形参上匹配效果不逊于任何候选函数。
若找到此类函数,则其无疑是最佳选择。若无法找到此类函数,则该调用将被视为歧义(或不匹配)。
例如:
#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');
return 0;
}
在上面的程序中,所有函数都与第一个实参完全匹配。然而,顶部的函数通过提升机制匹配第二个形参,而其他函数则需要进行转换。因此,print(char, int) 毫无疑问是最佳匹配。


浙公网安备 33010602011771号