为什么在其他函数中声明的函数不参与参数相关查找?

Why doesn't function declared inside other function participate in argument dependent lookup?

本文关键字:函数 参数 查找 其他 声明 为什么      更新时间:2023-10-16

考虑一个简单的例子:

template <class T>
struct tag { };
int main() {
auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
tag<int> bar(tag<int>);
bar(tag<int>{}); // <- compiles OK
foo(tag<int>{}); // 'bar' was not declared in this scope ?!
}
tag<int> bar(tag<int>) { return {}; }

[gcc] 和 [clang] 都拒绝编译代码。此代码是否以某种方式格式不正确?

foo(tag<int>{});触发了foo闭包类型的函数调用运算符成员函数模板的专用化隐式实例化,模板参数tag<int>。这将为此成员函数模板专用化创建一个实例化点。根据 [温度点]/1:

对于函数模板

专用化,成员函数模板 专用化,或成员函数或静态的专用化 类模板的数据成员(如果专用化是隐式的) 实例化,因为它是从另一个模板中引用的 专业化及其引用上下文取决于 模板参数,专业化的实例化点 是封闭专用化的实例化点。 否则,这种专业化的实例化点 紧跟在引用专用化的命名空间范围声明或定义之后

(强调我的)

因此,实例化的点紧接在main的定义之后,在bar的命名空间范围定义之前。

根据 [temp.dep.candidate ]/1 对decltype(bar(x))收益中使用的bar进行名称查找:

对于后缀表达式是依赖表达式的函数调用 名称,候选函数是使用通常的查找规则找到的 (6.4.1, 6.4.2) 但以下情况除外:

(1.1) — 对于使用非限定名称查找的查找部分 (6.4.1),仅来自模板定义的函数声明 找到上下文。

(1.2) — 对于使用关联命名空间进行查找的部分 (6.4.2),仅在模板中找到函数声明 找到定义上下文或模板实例化上下文。[...]

定义上下文中的普通非限定查找找不到任何内容。定义上下文中的 ADL 也找不到任何内容。实例化上下文中的 ADL,根据 [temp.point]/7:

表达式的实例化上下文,它依赖于 模板参数是具有外部链接的声明集 在模板实例化点之前声明 同一翻译单元的专业化。

同样,什么都没有,因为bar尚未在命名空间范围内声明。

因此,编译器是正确的。此外,请注意[温度点]/8:

函数模板

、成员函数模板的专用化, 或成员函数或静态数据的成员类模板可以 在一个翻译单元中有多个实例化点,以及 除了上述实例化点外,对于任何 这种专业化在 翻译单元,翻译单元的结束也考虑 一个实例化点。类模板的专用化具有 一个翻译单元中最多一个实例化点。一个 任何模板的专用化都可以在 多个翻译单元。如果两个不同的实例化点根据一个定义规则赋予模板专用化不同的含义 (6.2),程序格式不正确,无需诊断。

(强调我的)

以及 [temp.dep.candidate ]/1 的第二部分:

[...]如果调用格式不正确或会找到更好的匹配项 关联命名空间中的查找考虑了所有 在这些中引入的带有外部链接的函数声明 所有翻译单元中的命名空间,而不仅仅是考虑这些 在模板定义和模板中找到的声明 实例化上下文,则程序具有未定义的行为。

因此,格式不正确的 NDR 或未定义的行为,任您选择。


让我们考虑一下您上面评论中的示例:

template <class T>
struct tag { };
auto build() {
auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
return foo;
}
tag<int> bar(tag<int>) { return {}; }
int main() {
auto foo = build();
foo(tag<int>{});
}

在定义上下文中查找仍然找不到任何内容,但实例化上下文紧跟在main的定义之后,因此该上下文中的 ADL 在全局命名空间(与tag<int>相关联)中找到bar,代码编译。


让我们也考虑一下 AndyG 上面评论中的例子:

template <class T>
struct tag { };
//namespace{
//tag<int> bar(tag<int>) { return {}; }
//}
auto build() {
auto foo = [](auto x) -> decltype(bar(x)) { return {}; };
return foo;
}
namespace{
tag<int> bar(tag<int>) { return {}; }
}
int main() {
auto foo = build();
foo(tag<int>{});
}

同样,实例化点紧跟在main的定义之后,那么为什么bar不可见呢?未命名的命名空间定义在其封闭命名空间(在本例中为全局命名空间)中引入了该命名空间的using 指令。这将使bar对普通非限定查找可见,但根据 [basic.lookup.argdep]/4,对 ADL 不可见:

考虑关联的命名空间时,查找与 将关联的命名空间用作 限定符 (6.4.3.2) 除外:

(4.1) — 关联命名空间中的任何using 指令都是 忽视。[...]

由于在实例化上下文中仅执行查找的 ADL 部分,因此未命名命名空间中的bar不可见。

注释掉下部定义并取消注释上部定义会使未命名命名空间中的bar对定义上下文中的普通非限定查找可见,因此代码会编译。


让我们也考虑一下您上面其他评论中的示例:

template <class T>
struct tag { };
int main() {
void bar(int);
auto foo = [](auto x) -> decltype(bar(decltype(x){})) { return {}; };
tag<int> bar(tag<int>);
bar(tag<int>{});
foo(tag<int>{});
}
tag<int> bar(tag<int>) { return {}; }

这被GCC接受,但被Clang拒绝。虽然我最初很确定这是 GCC 中的一个错误,但答案实际上可能并不那么明确。

块范围的声明void bar(int);根据 [basic.lookup.argdep]/3 禁用 ADL:

设 X 是由非限定查找 (6.4.1) 生成的查找集,并让 Y 是由参数相关查找生成的查找集(定义为 以下)。如果 X 包含

(3.1) — 集体成员的声明,或

(3.2) —不是 使用声明,或

(3.3) — 既不是函数也不是函数的声明 模板

则 Y 为空。[...]

(强调我的)

现在,问题是这是在定义和实例化上下文中禁用 ADL,还是仅在定义上下文中禁用 ADL。

如果我们认为 ADL 在两种情况下都已禁用,则:

  • 块作用域声明(在定义上下文中对普通非限定查找可见)是闭包类型的成员函数模板专用化的所有实例化唯一可见的声明。Clang的错误消息,即没有可行的int转换,是正确的,也是必需的 - 上面关于格式错误的NDR和未定义行为的两个引号不适用,因为在这种情况下,实例化上下文不会影响名称查找的结果。
  • 即使我们将bar命名空间范围定义移到main以上,代码仍然无法编译,原因与上面相同:当它找到块范围声明void bar(int);并且不执行 ADL 时,普通的非限定查找会停止。

如果我们认为仅在定义上下文中禁用 ADL,则:

  • 就实例化上下文而言,我们回到第一个示例;ADL 仍然找不到bar的命名空间范围定义。但是,上面的两个引号(格式错误的 NDR 和 UB)确实适用,因此我们不能责怪编译器没有发出错误消息。
  • bar的命名空间范围定义移到main上方会使代码格式正确。
  • 这也意味着实例化上下文中的 ADL 始终针对依赖名称执行,除非我们以某种方式确定表达式不是函数调用(通常涉及定义上下文......

看看 [temp.dep.candidate]/1 的措辞,它似乎说纯非限定查找仅在定义上下文中执行作为第一步,然后根据 [basic.lookup.argdep] 中的规则在两个上下文中执行 ADL。这意味着普通无条件查找的结果会影响整个第二步,这使我倾向于第一个选项。

此外,支持第一个选项的更有力的论据是,当 [basic.lookup.argdep]/3.1 或 3.3 在定义上下文中适用时,在实例化上下文中执行 ADL 似乎没有意义。

还。。。在性病讨论中可能值得询问这个问题。


所有报价均来自当前标准草案N4713。

来自非限定查找规则 ([basic.lookup.unqual]):

对于类X的成员,成员函数体中使用的名称 [...] 应在其中一个 以下方法
— 如果X是局部类或局部类的嵌套类,则在块中定义类之前X附上类X的定义

你的泛型 lambda 是main中的一个局部类,所以要使用bar,名称bar必须事先出现在声明中。