我在以下代码中遇到过,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>);
现场演示:https://godbolt.org/z/hK6xhesKM
foo()
是在全局命名空间中声明的,具有推导的返回类型。S<U>
通过友元函数为foo()
提供定义,在友元函数中返回类型为U
的值。
我所期望的是,当S
用U=double
实例化时,它对foo()
的定义被放在全局命名空间中,U
被替换,这是因为友元函数的工作方式,实际上是这样的:
template<typename T>
auto foo(T);
S<double> s;
template<typename T>
auto foo(T) {
return double{};
}
因此,我预计foo()
的返回类型是double
,并且应该通过以下静态断言:
static_assert(std::same_as<decltype(foo(42)), double>);
然而,实际发生的情况是,三位编译器在行为上都存在分歧。
GCC传递静态断言,正如我所期望的那样。
Clang使静态断言失败,因为它似乎认为foo()
返回int
:
'std::is_same_v<int,双精度>'评估为错误
MSVC产生了一个完全不同的错误:
'U':未声明的标识符
对于一个看似简单的代码示例,所有编译器在这里都有不同的行为,这让我觉得很奇怪
如果foo()
没有模板化(请参阅此处的演示),或者没有推导出的返回类型(请参阅这里的演示)则不会出现此问题。
哪个编译器具有正确的行为?或者,代码是格式错误的NDR还是未定义的行为?(为什么?)
哪个编译器具有正确的行为?或者,代码是格式错误的NDR还是未定义的行为?(为什么?)
正如@Sedenion在评论中指出的那样,在谈到CWG 2118的领域时(我们将进一步回到这一点),该程序符合当前标准,GCC接受它是正确的,而Clang和MSVC拒绝它是不正确的,这是由[dcl.spec.auto.general]/12和/13[emphasismine]管辖的:
/12返回模板化实体的类型推导,该实体是函数或函数模板,其声明类型中包含占位符即使函数体包含具有非类型相关操作数的返回语句,定义实例化时也会发生。
/13函数或函数模板的重新声明或专业化带有使用占位符类型的声明返回类型的使用占位符,而不是推导类型。类似地,重新声明或函数或函数模板的特殊化不使用占位符类型的返回类型不应使用占位符。
[示例6:
PD_ 4
;不是CCD_ 14的朋友";/13的示例强调,重新声明应使用占位符类型(frf(T)
),而不是推导类型(frf(int)
),而否则该特定示例将有效。
/12,以及[temp.inst]/3(如下)涵盖了只有在从S<double>
封装类模板专用化的实例化中使朋友的主模板定义可用(形式上:它的声明但不是定义已实例化)之后,才会对朋友进行返回类型推导。
类模板专业化的隐式实例化导致
- (3.1)未删除类成员函数、成员的声明的隐式实例化,而不是定义的显式实例化类,作用域成员枚举,静态数据成员,成员模板和好友;以及
- […]
但是,为了确定根据〔basic.def.odr〕和〔class.mem〕,重新声明是有效的与模板中的定义相对应的声明是被认为是一个定义。
[示例4:
// ... template<typename T> struct Friendly { template<typename U> friend int f(U) { return sizeof(T); } }; Friendly<char> fc; Friendly<float> ff; // error: produces second definition of f(U)
--结束示例]
CWG 2118:通过友元注入的状态元编程
正如在《愚蠢的树叶》中详细介绍的那样,可以依靠类模板的第一个实例来控制朋友的定义First这里的意思是本质上依赖元编程状态来指定这个定义。
在OP的示例中,第一个实例化S<double>
用于设置朋友foo
的主模板的定义,使得对于朋友的所有专业化,其推导的类型将始终推导为double
。如果我们(在整个程序中)隐式或显式地实例化S
的第二个实例化(忽略专门用于删除好友的S
),我们就会遇到ODR违规和未定义的行为。这意味着,在实践中,这种程序基本上是无用的,因为它会为客户端提供盘片上未定义的行为,但正如上面链接的文章所涵盖的,它可以用于实用程序类,以绕过私有访问规则(同时仍然是完全良好的)或其他黑客机制,如有状态元编程(通常会进入或超出良好形式的灰色区域)。