关于默认参数的第二阶段名称查找的问题

jac*_*k X 21 c++ templates language-lawyer c++17

#include <iostream>
namespace J {
    template <typename T> void zip(int = zap([] { })) { }  //#1
    template <typename T> int zap(const T &t) {return 0; }
}
int main(){
    J::zip<long>(); 
}
Run Code Online (Sandbox Code Playgroud)

考虑上面的代码,这是建议的分辨率 1664的简化示例。注意标有 的地方#1,我怀疑为什么zap可以在实例化的上下文中查找名称。我认为zap不是依赖名,依赖名的定义如下:
temp.dep

在以下形式的表达式中:

postfix-expression ( expression-list opt )
其中 postfix-expression 是不合格的 id,不合格的 id 表示依赖名称,如果

  • 表达式列表中的任何表达式都是包扩展,
  • 表达式列表中的任何表达式或花括号初始化器列表都是类型相关的,或者
  • unqualified-id 是一个模板 ID,其中任何模板参数都依赖于模板参数。

zap([] { })由于表达式的类型[] { }不是依赖类型,我认为不满足上述任何条件。尽管以下规则说与closure-type 关联的命名空间确定如下:
temp.inst#11

如果以需要使用默认参数的方式调用函数模板 f,则查找依赖名称,检查语义约束,并且默认参数中使用的任何模板的实例化都像默认参数一样完成已经是在函数模板特化中使用的初始化器,与当时使用的函数模板 f 具有相同的作用域、相同的模板参数和相同的访问权限,除了声明闭包类型的作用域([expr. prim.lambda.closure])——因此它的关联命名空间——仍然是由默认参数定义的上下文确定。这种分析称为默认参数实例化。实例化的默认参数然后用作 f 的参数。

但是,这些来自模板定义上下文和实例化上下文的名称仅考虑用于依赖名称,由以下规则决定
temp.dep.res

在解析从属名称时,会考虑来自以下来源的名称:

  • 在模板定义点可见的声明。
  • 来自与来自实例化上下文 ([temp.point]) 和定义上下文的函数参数类型相关联的命名空间声明。

temp.nondep

模板定义中使用的非依赖名称是使用通常的名称查找找到的,并在使用时绑定

所以,我认为,为了遵守上述规则,名称查找zap只发生在它被使用的点(即 at #1),因为它不是依赖名称,即来自实例化上下文的名称( ADL) 根本不考虑。

我在三个实现中测试了代码,结果列在下面:

  1. Clang9.0 及更高版本视图zap作为依赖名称。
  2. Clang 8.0以下的版本报错,无意义。
  3. 版本下9.1的gcc的意见zap作为从属名称。
  4. 高于9.1gcc版本将其zap视为非依赖名称,并且不会zap在实例化的上下文中执行名称查找。

那么,名称查找的具体过程是什么zap?似乎最新版本的 GCC 同意将其视为zap非依赖名称,这导致它找不到zap. 如果我错过了标准中的其他规则,我将不胜感激您指出。

sig*_*gma 1

我认为您的诊断是正确的,特别是考虑到 [temp.nondep] 中的示例非常相似。

At#1zap在模板定义内的函数调用表达式中使用的非限定名称。它不是从属名称,因此必须在定义时绑定它。目前还没有::J::zap或在范围内,ADL 也无法找到它。::zap例如,如果参数类型是在名称空间中声明的,而该名称空间会被非限定名称查找忽略,则情况会有所不同:

namespace I { 
struct nondep {};

template <typename T>
int zap(const T&) { return 0; } 
}

namespace J {
template <typename T>
void zip(int = zap(I::nondep{})) { }  // #1
// 1. Unqualified name lookup of zap finds nothing,
// 2. ADL considers namespace I and finds only I::zap,
// 3. Overload resolution succeeds:
// zap is bound to I::zap<I::nondep>
    
template <typename T>
int zap(const T&) { return 0; } // Not considered
}

template <typename T>
int zap(const T&) { return 0; } // Not considered

int main() {
    J::zip<long>(); 
}
Run Code Online (Sandbox Code Playgroud)

为了说明依赖名称的差异,下面是一个稍作修改的示例:

namespace J {
struct nondep {};

template <typename T>
int zap(T) { return 2; }

template <typename T>
int zip(int i = zap(T{})) { return i; }  // #1, zap is dependent
}

struct nondep {};

template<typename T> int zap(T) { return 1; }

int main() {
    // Unqualified name lookup for zap finds J::zap, 
    // ADL additionally finds ::zap,
    // Overload resolution fails:
    // this call to zap would be ambiguous
    // int x = J::zip<nondep>(); 

    // But here, unqualified lookup and ADL only find J::zap, which is selected
    int y = J::zip<J::nondep>(); // y == 2
}
Run Code Online (Sandbox Code Playgroud)

不过,我并不完全确定 lambda 闭包类型的范围。根据[expr.prim.lambda.closure]中的措辞,

闭包类型在包含相应lambda 表达式的最小块作用域、类作用域或命名空间作用域中声明。

它没有提到函数参数范围,它必须将其放置在名称空间 J 中,尽管无论如何,我认为编译器应该能够确定它[]{}不依赖于typename T.