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

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

考虑一个简单的例子:

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] and [clang]拒绝编译代码。这段代码在某种程度上是错误的吗?

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

For the members of a class X, a name used in a member function body, [...], shall be declared in one of the following ways
— if X is a local class or is a nested class of a local class, before the definition of class X in a block enclosing the definition of class X

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

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

For a function template specialization, a member function template specialization, or a specialization for a member function or static data member of a class template, if the specialization is implicitly instantiated because it is referenced from within another template specialization and the context from which it is referenced depends on a template parameter, the point of instantiation of the specialization is the point of instantiation of the enclosing specialization. Otherwise, the point of instantiation for such a specialization immediately follows the namespace scope declaration or definition that refers to the specialization.

(强调我的)

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

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

进行

For a function call where the postfix-expression is a dependent name, the candidate functions are found using the usual lookup rules (6.4.1, 6.4.2) except that:

(1.1) — For the part of the lookup using unqualified name lookup (6.4.1), only function declarations from the template definition context are found.

(1.2) — For the part of the lookup using associated namespaces (6.4.2), only function declarations found in either the template definition context or the template instantiation context are found. [...]

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

The instantiation context of an expression that depends on the template arguments is the set of declarations with external linkage declared prior to the point of instantiation of the template specialization in the same translation unit.

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

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

A specialization for a function template, a member function template, or of a member function or static data member of a class template may have multiple points of instantiations within a translation unit, and in addition to the points of instantiation described above, for any such specialization that has a point of instantiation within the translation unit, the end of the translation unit is also considered a point of instantiation. A specialization for a class template has at most one point of instantiation within a translation unit. A specialization for any template may have points of instantiation in multiple translation units. If two different points of instantiation give a template specialization different meanings according to the one-definition rule (6.2), the program is ill-formed, no diagnostic required.

(强调我的)

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

[...] If the call would be ill-formed or would find a better match had the lookup within the associated namespaces considered all the function declarations with external linkage introduced in those namespaces in all translation units, not just considering those declarations found in the template definition and template instantiation contexts, then the program has undefined behavior.

因此,格式不正确的 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 在全局命名空间中找到 bar(关联与 tag<int>) 和代码编译。


让我们也考虑上面 中 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 不可见

When considering an associated namespace, the lookup is the same as the lookup performed when the associated namespace is used as a qualifier (6.4.3.2) except that:

(4.1) — Any using-directives in the associated namespace are ignored. [...]

由于在实例化上下文中仅执行查找的 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

Let X be the lookup set produced by unqualified lookup (6.4.1) and let Y be the lookup set produced by argument dependent lookup (defined as follows). If X contains

(3.1) — a declaration of a class member, or

(3.2) — a block-scope function declaration that is not a using-declaration, or

(3.3) — a declaration that is neither a function nor a function template

then Y is empty. [...]

(强调我的)

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

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

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

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

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

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

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

不过...可能值得在 std-discussion.

上询问这个问题

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