【问题标题】:A issue about the second phase name lookup for default argument关于默认参数的第二阶段名称查找的问题
【发布时间】:2020-12-10 10:04:52
【问题描述】:
#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>(); 
}

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

在一个表达式中:

后缀表达式(表达式列表opt
如果后缀表达式是非限定 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 的名称查找只发生在使用它的点(即#1),因为它不是从属名称,即,则根本不考虑实例化(ADL)上下文中的名称。

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

  1. Clang9.0 and higher versionzap 视为从属名称。
  2. version under 8.0 of Clang 报告了一个没有意义的错误。
  3. version under 9.1 of gcczap 视为从属名称。
  4. version higher than 9.1 of gcczap 视为非依赖名称,并且不会在实例化上下文中执行 zap 的名称查找。

那么,zap 的名称查找究竟经历了什么过程?似乎最新版本的 GCC 同意将 zap 视为非依赖名称,这导致它无法找到 zap 的名称。如果我错过了标准中的其他规则,我会感谢您指出。

【问题讨论】:

  • 好吧,所有“三个”编译器似乎都同意[] { } 指的是zip 的每个特化的不同类,所以也许它应该被认为依赖于T。不知道在哪里/如果那在标准中。 godbolt.org/z/M1d875
  • @HTNW 因为[]{}的类型不同,请注意as if the default argument had been an initializer used in a function template specialization。所以对于每个特化trigger&lt;int&gt;trigger&lt;char&gt;,表达式[]{}的闭包类型是不同的,因为它们在它们的函数模板特化中分别是不同的本地类类型。但是 [temp.dep.type] 中没有任何规则说函数模板的本地类类型应该被视为依赖类型。
  • zip 和 zap 函数定义的交换行解决了所有编译错误...
  • @Cloud 关键是默认参数中的lambda表达式的闭包类型是否可以被认为是依赖类型。

标签: c++ templates c++17 language-lawyer


【解决方案1】:

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

#1zap 是在模板定义内的函数调用表达式中使用的非限定名称。它不是从属名称,因此必须在定义点进行绑定。范围内没有::J::zap::zap(目前),ADL 目前也找不到它。例如,如果参数类型是在一个名称空间中声明的,则会被非限定名称查找忽略:

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>(); 
}

为了说明依赖名称的区别,这里有一个稍微修改的例子:

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
}

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

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

它没有提到函数参数范围,它必须将它放在命名空间J中,尽管不管这一点,我认为编译器应该能够确定[]{}不依赖于typename T

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-09
    • 2021-05-23
    • 2015-03-18
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    相关资源
    最近更新 更多