【问题标题】:Why doesn't function declared inside other function participate in argument dependent lookup?为什么在其他函数中声明的函数不参与参数相关查找?
【发布时间】:2018-02-02 15:55:08
【问题描述】:

考虑一个简单的例子:

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] 都拒绝编译代码。这段代码在某种程度上是不是格式错误?

【问题讨论】:

  • 是的,我认为 fwd 声明应该出现在通用 lambda 之前。
  • @AndyG 那么为什么this 编译呢?
  • @AndyG 实际上,它需要在main() 之外声明才能正常工作:Sample1, Sample2
  • @W.F.好吧,是的,但是无论如何这里都考虑了全局名称空间。 ADL 用于根据参数的名称空间添加要考虑的其他名称空间,不是吗?对我来说看起来像是普通的不合格查找,不受 ADL 额外规则的影响。
  • @W.F.因为不合格的查找/范围哈哈 :-) Check this out 注释底部匿名命名空间,然后取消注释上面的。

标签: c++ templates c++14 language-lawyer argument-dependent-lookup


【解决方案1】:

foo(tag&lt;int&gt;{}); 使用模板参数tag&lt;int&gt; 触发foo 闭包类型的函数调用运算符成员函数模板的隐式实例化。这会为此成员函数模板特化创建一个实例化点。根据[temp.point]/1:

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

(强调我的)

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

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

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

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

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

在定义上下文中的普通不合格查找没有找到任何东西。定义上下文中的 ADL 也找不到任何内容。实例化上下文中的 ADL,根据 [temp.point]/7:

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

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

所以,编译器是正确的。此外,请注意 [temp.point]/8:

函数模板的特化,成员函数模板, 或类模板的成员函数或静态数据成员 在翻译单元中有多个实例化点,并且 除了上述实例化点之外,对于任何 这种特殊化在 翻译单元,翻译单元的结尾也被考虑 一个实例化点。类模板的特化有 翻译单元内最多有一个实例化点。一种 任何模板的特化都可能有实例化点 多个翻译单元。 如果两个不同的实例化点根据单一定义规则赋予模板特化不同的含义 (6.2),程序格式错误,不需要诊断。

(强调我的)

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

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

因此,格式错误的 NDR 或未定义的行为,请自行选择。


让我们考虑上面your comment的例子:

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&lt;int&gt; 关联)中找到bar 并且代码编译。


让我们也考虑一下上面his comment中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-directive。根据 [basic.lookup.argdep]/4,这将使 bar 对普通的不合格查找可见,但对 ADL 不可见:

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

(4.1) — 关联命名空间中的任何 using-directives 都是 忽略。 [...]

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

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


让我们也考虑一下上面other comment 中的示例:

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 在两种情况下都被禁用,那么:

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

如果我们只在定义上下文中考虑禁用 ADL,那么:

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

看[temp.dep.candidate]/1是怎么写的,好像是说plain unqualified lookup只在定义上下文中作为第一步进行,然后按照[basic中的规则进行ADL .lookup.argdep] 作为第二步。这意味着普通不合格查找的结果会影响整个第二步,这使我倾向于第一个选项。

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

仍然......可能值得在std-discussion 上询问这个问题。


所有引用均来自N4713,当前的标准草案。

【讨论】:

  • 这绝对可以解决问题。通过我自己对标准的分析,我可能永远不会想到这一点。我想我明白了,谢谢!
【解决方案2】:

来自不合格的查找规则([basic.lookup.unqual]):

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

您的通用 lambda 是 main 中的本地类,因此要使用 bar,名称 bar 必须事先出现在声明中。

【讨论】:

  • 我很困惑,因为我认为this 不应该也编译,不是吗?
  • @W.F. ADL ftw!安迪的引用意味着这个名字必须存在 - 这并不意味着最终会被调用。
  • @Barry ahhh 现在一切都说得通了。谢谢!
  • 我不知道这是否能说服我。例如,这不会被拒绝auto f() { return [](auto x) -&gt; decltype(bar(x)) { return; }; } struct A { }; void bar(A) { } int main() { f()(A{}); },但根据您的回答,它的格式同样不正确。请注意注释“[ 注意:模板定义中名称查找的规则在 [temp.res] 中进行了描述。— end note ]”这意味着它实际上打算让您引用的规则不适用于 lambdas 对 bar 的使用,因为它出现在成员函数模板的体中,而不是成员函数的体中。
  • 事实上,即使某些规则将适用于模板定义并使其格式错误:如果模板具有有效的特化,则不必生成诊断并且程序必须根据“否则,不会为可以生成有效专业化的模板发出诊断。”.
猜你喜欢
  • 2011-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多