重载解析为尚不可见的函数

Overload resolution resolves to a function not visible yet

本文关键字:函数 重载      更新时间:2023-10-16

这是这个问题的后续。

#include <iostream>
struct type1 {};
struct type2 {};
void foo(type1 x)
{
std::cout << "foo(type1)" << std::endl;
}
template<typename T>
void bar() {
foo(T());
}
int main()
{
bar<type1>();
bar<type2>();
return 0;
}
void foo(type2 x)
{
std::cout << "foo(type2)" << std::endl;
}

在上面的代码中,foo(type2)main中实例化bar<type2>时不可见。然而,代码编译并生成以下输出:

foo(type1)
foo(type2)

编译器如何知道在main中实例化bar<type2>foo(type2)可用?

编辑:我正在尝试更多地了解模板实例化期间的重载解析如何工作。考虑下面的代码:

#include <iostream>
struct type1 {};
struct type2 {};
struct type3 {
operator type2() { return type2(); }
};
void foo(type1 x)
{
std::cout << "foo(type1)" << std::endl;
}
void foo(type2 x)
{
std::cout << "foo(type2)" << std::endl;
}
int main()
{
foo(type3());
return 0;
}
void foo(type3 x)
{
std::cout << "foo(type3)" << std::endl;
}

输出为

foo(type2)

即使有更紧密的匹配foo(type3)可用,调用foo(type3())也会解析为foo(type2),因为这是编译器在此之前分析的唯一候选项。现在考虑以下代码:

#include <iostream>
struct type1 {};
struct type2 {};
struct type3 {
operator type2() { return type2(); }
};
void foo(type2 x)
{
std::cout << "foo(type2)" << std::endl;
}
template<typename T>
void bar() {
foo(T());
}
int main()
{
bar<type3>();
return 0;
}
void foo(type3 x)
{
std::cout << "foo(type3)" << std::endl;
}

输出为

foo(type3)

也就是说,在调用bar<type3>()时,即使只有foo(type2)可见,编译器仍然会选择稍后出现的foo(type3)因为这是更接近的匹配。

任何没有定义的符号都将在链接过程中被替换,因为该函数foo(type2)可以在另一个文件中提供。

编译器是说,当不能应用进一步的替换时,在整个过程结束时是否已经定义了所需的函数。

为了澄清理解,您必须了解编译通用 C 程序所需的步骤:

  • 首先,展开代码上的所有宏;

  • 然后根据语言语法验证你的代码,以便它可以转换为汇编语言——编译过程本身; 在此步骤中,每个没有定义的符号都会在带有条目的表中进行注释(symbol, definition),这将在以后完成,从而可以正确构建您的程序;

  • 接下来,编译成汇编的代码将被转换为机器语言,即创建对象;

  • 最后,您需要链接已经可执行的对象,以解决对符号定义的任何依赖关系;最后一步检查对象中是否存在未定义的符号,从其他模块或库添加定义,从而完成程序。

如果任何符号没有正确"链接"到其定义,编译器将指出程序中的错误 - 经典undefined reference to...

考虑到您发布的代码,该过程将被执行,直到它到达编译器。编译器将遍历代码,注意type1type2foo(type1 x)bar<T>()的定义。

struct type1 {};
struct type2 {};

当它到达主时,它会找到bar<type1>();的调用,并调用foo(type1()),这是已知的,可以正确使用。

void foo(type1 x) {
std::cout << "foo(type1)" << std::endl;
}
template<typename T>
void bar() {
foo(T());
}
int main() {
bar<type1>();
bar<type2>();
return 0;
}

一旦它到达下一个调用,bar<type2>();,它将尝试调用foo(type2()),但没有这样的定义可供使用,所以它会将此调用关联为未知符号,必须在后面的进程中替换为定义。

编译器运行main后,它到达一个新的定义,也就是正在创建的"转换表"上缺少的定义。

void foo(type2 x) {
std::cout << "foo(type2)" << std::endl;
}

因此,在下一步中,编译能够将符号替换为其各自的定义,并且程序可以正确编译。

问候!

答案是通过参数相关的名称查找(ADL)找到的(在链接的问题中也提到了)。foo(T());有两个查找。首先在模板定义时,在定义点定义的任何函数都包含在重载集中。这意味着当编译器看到bar内部的foo(T());时,它只会在重载集中添加void foo(type1 x)。但是,执行了第二个查找,称为 ADL。在模板实例化时,即bar<type2>();,它会在与提供的参数相同的命名空间中查找foo,在本例中为type2。由于type2位于全局命名空间中,因此它会查找一个foo,该在全局命名空间中获取type2并找到它,并解析调用。如果要查找标准中的信息,请参阅14.6.4.2 Candidate functions

请尝试以下操作并观察代码失败。这是因为它无法在与a::type1相同的命名空间中找到foo

#include <iostream>
namespace a
{
struct type1 {};
}
template<typename T>
void bar() {
foo(T());
}
int main()
{
bar<a::type1>();
return 0;
}
void foo(a::type1 x)
{
std::cout << "foo(a::type1)" << std::endl;
}