foo(tag<int>{}); 使用模板参数tag<int> 触发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<int> 关联)中找到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,当前的标准草案。