友元函数模板在类模板中返回推导的依赖类型的行为

MC *_* ΔT 6 c++ templates language-lawyer c++20

我遇到过以下代码,其行为得到了 GCC、Clang 和 MSVC 的一致认可:

#include <concepts>

template<typename T>
auto foo(T);

template<typename U>
struct S {
    template<typename T>
    friend auto foo(T) {
        return U{};
    }
};

S<double> s;
static_assert(std::same_as<decltype(foo(42)), double>);
Run Code Online (Sandbox Code Playgroud)

现场演示: https: //godbolt.org/z/hK6xhesKM

foo()在全局命名空间中声明并推导返回类型。S<U>提供了 via 友元函数的定义foo(),它返回类型为 的值U

我期望的是,当S用 实例化时U=double,它的定义foo()被放入全局命名空间中U并被替换,由于友元函数的工作方式,有效地如下所示:

template<typename T>
auto foo(T);

S<double> s;

template<typename T>
auto foo(T) {
    return double{};
}
Run Code Online (Sandbox Code Playgroud)

因此我期望foo()的返回类型是double,并且以下静态断言应该通过:

static_assert(std::same_as<decltype(foo(42)), double>);
Run Code Online (Sandbox Code Playgroud)

然而,实际发生的情况是,所有三个编译器对此行为都持不同意见。

正如我所期望的,GCC 通过了静态断言。

Clang 失败了静态断言,因为它似乎相信foo()返回int

'std::is_same_v<int, double>' 评估为 false

MSVC 会产生完全不同的错误:

'U':未声明的标识符

我觉得奇怪的是,对于一个看似简单的代码示例,所有编译器都有不同的行为。如果未模板化(请参阅此处的演示),或者没有推导的返回类型(请参阅此处的演示
),则 不会出现此问题。foo()

哪个编译器有正确的行为?或者,代码是否格式不正确的 NDR 或未定义的行为?(而且,为什么?)

dfr*_*fri 8

\n

哪个编译器有正确的行为?或者,代码是否格式不正确的 NDR 或未定义的行为?(而且,为什么?)

\n
\n

正如 @Sedenion 在评论中指出的那样,在涉及CWG 2118的领域时(我们将在下面进一步讨论),该程序符合当前标准,并且 GCC 正确接受它,而 Clang 和 MSVC根据[dcl.spec.auto.general]/12/13 [强调我的] 的规定,拒绝它是不正确的:

\n
\n

/12模板化实体是声明类型中带有占位符的函数或函数模板\n 的返回类型推导,即使函数体包含带有非类型相关操作数的 return 语句,在实例化定义时也会发生。

\n

/13 带有使用占位符类型的声明返回类型的函数或函数模板的重新声明或特化\n也应\n使用该占位符,而不是推导类型。同样,具有不使用占位符类型的声明返回类型的函数或函数模板的重新声明或\n特化不应使用占位符。

\n

[实施例6:

\n
auto f();\nauto f() { return 42; }            // return type is int\nauto f();                          // OK\n// ...\ntemplate <typename T> struct A {\n  friend T frf(T);\n};\nauto frf(int i) { return i; }      // not a friend of A<int>\n
Run Code Online (Sandbox Code Playgroud)\n
\n

特别是 /13 的“不是朋友A<int>”示例强调重新声明应使用占位符类型 ( frf(T),而不是推导类型 ( frf(int)) ,否则该特定示例将是有效的。

\n

/12 以及[temp.inst]/3(如下)涵盖了仅在好友的主模板定义可用之后才会发生好友的返回类型推导(正式来说:它是声明,而不是定义已实例化)来自S<double>封闭类模板特化的实例化。

\n
\n

类模板特化的隐式实例化导致

\n
    \n
  • (3.1)未删除类成员函数、成员类、作用域成员枚举、静态数据成员、成员模板和友元的声明的隐式实例化,但不是定义的隐式实例化;和
  • \n
  • [...]
  • \n
\n

然而,为了根据 [basic.def.odr] 和 [class.mem] 确定实例化的重新声明是否有效,与模板中的定义相对应的声明被视为定义。

\n

[示例4:

\n
// ...\n\ntemplate<typename T> struct Friendly {\n  template<typename U> friend int f(U) { return sizeof(T); }\n};\nFriendly<char> fc;\nFriendly<float> ff;  // error: produces second definition of f(U)\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\x94结束示例]

\n
\n
\n

CWG 2118:通过友元注入进行状态元编程

\n

正如A foliage of folly中详细介绍的那样,人们可以依靠类模板的单个第一个实例来控制朋友的定义的外观。首先这里意味着本质上依赖于元编程状态来指定这个定义。

\n

在 OP 的示例中,第一个实例化S<double>,用于设置友元的主模板的定义foo,以便double对于该友元的所有特化,其推导类型将始终推导为 。如果我们(在整个程序中)隐式或显式实例化第二个实例S(忽略S专门删除友元),我们就会遇到 ODR 违规和未定义的行为。这意味着这种程序在实践中本质上是无用的,因为它会为客户端提供未定义的行为,但正如上面链接的文章中所介绍的,它可以用于实用程序类来规避私有访问规则(同时仍然完全格式良好)或其他黑客机制,例如状态元编程(通常会进入或超出格式良好的灰色区域)。

\n