Vai*_*Man 6 c++ crtp language-lawyer c++-concepts c++20
Clang 拒绝这个演示,而 GCC 和 MSVC 接受它。(https://godbolt.org/z/M1Wsxs8fj)
谁是正确的?或者这是不正常的不需要诊断?
#include <type_traits>
#if 0
#define METHOD balabala // OK
#else
#define METHOD operator= // Clang error
#endif
struct base {};
template<typename T>
concept derived = std::is_base_of_v<base, T>;
template<derived Lhs, derived Rhs> struct assign;
template<typename T>
struct crtp: base {
template<derived Rhs>
constexpr auto METHOD(const Rhs& rhs) -> assign<T, Rhs> {
return {};
}
};
template<typename T>
struct foo: crtp<foo<T>> {
using crtp<foo<T>>::METHOD;
};
template<derived Lhs, derived Rhs>
struct assign: crtp<assign<Lhs, Rhs>> { };
static_assert(std::is_base_of_v<base, foo<int>>);
Run Code Online (Sandbox Code Playgroud)
铿锵消息:
<source>:12:19: error: substitution into constraint expression resulted in a non-constant expression
concept derived = std::is_base_of_v<base, T>;
^~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:14:10: note: while checking the satisfaction of concept 'derived<foo<int>>' requested here
template<derived Lhs, derived Rhs> struct assign;
^
<source>:14:10: note: while substituting template arguments into constraint expression here
template<derived Lhs, derived Rhs> struct assign;
^~~~~~~
<source>:19:44: note: while checking constraint satisfaction for template 'assign<foo<int>, crtp<foo<int>>>' required here
constexpr auto METHOD(const Rhs& rhs) -> assign<T, Rhs> {
^~~~~~~~~~~~~~
<source>:17:8: note: while substituting deduced template arguments into function template 'operator=' [with Rhs = crtp<foo<int>>]
struct crtp: base {
^
<source>:25:8: note: while declaring the implicit copy assignment operator for 'foo<int>'
struct foo: crtp<foo<T>> {
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/type_traits:3361:68: note: in instantiation of template class 'foo<int>' requested here
inline constexpr bool is_base_of_v = __is_base_of(_Base, _Derived);
^
<source>:32:20: note: in instantiation of variable template specialization 'std::is_base_of_v<base, foo<int>>' requested here
static_assert(std::is_base_of_v<base, foo<int>>);
^
<source>:12:19: note: initializer of 'is_base_of_v<base, foo<int>>' is unknown
concept derived = std::is_base_of_v<base, T>;
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/type_traits:3361:25: note: declared here
inline constexpr bool is_base_of_v = __is_base_of(_Base, _Derived);
^
<source>:12:19: error: substitution into constraint expression resulted in a non-constant expression
concept derived = std::is_base_of_v<base, T>;
^~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:14:10: note: while checking the satisfaction of concept 'derived<foo<int>>' requested here
template<derived Lhs, derived Rhs> struct assign;
^
<source>:14:10: note: while substituting template arguments into constraint expression here
template<derived Lhs, derived Rhs> struct assign;
^~~~~~~
<source>:19:44: note: while checking constraint satisfaction for template 'assign<foo<int>, crtp<foo<int>>>' required here
constexpr auto METHOD(const Rhs& rhs) -> assign<T, Rhs> {
^~~~~~~~~~~~~~
<source>:17:8: note: while substituting deduced template arguments into function template 'operator=' [with Rhs = crtp<foo<int>>]
struct crtp: base {
^
<source>:25:8: note: while declaring the implicit move assignment operator for 'foo<int>'
struct foo: crtp<foo<T>> {
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/type_traits:3361:68: note: in instantiation of template class 'foo<int>' requested here
inline constexpr bool is_base_of_v = __is_base_of(_Base, _Derived);
^
<source>:32:20: note: in instantiation of variable template specialization 'std::is_base_of_v<base, foo<int>>' requested here
static_assert(std::is_base_of_v<base, foo<int>>);
^
<source>:12:19: note: initializer of 'is_base_of_v<base, foo<int>>' is unknown
concept derived = std::is_base_of_v<base, T>;
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/13.0.1/../../../../include/c++/13.0.1/type_traits:3361:25: note: declared here
inline constexpr bool is_base_of_v = __is_base_of(_Base, _Derived);
^
2 errors generated.
Run Code Online (Sandbox Code Playgroud)
Clang 错误消息中的回溯解释了它在做什么:
\nnote: while substituting deduced template arguments into function template \'operator=\' [with Rhs = crtp<foo<int>>]\nstruct crtp : base {\n ^\nnote: while declaring the implicit copy assignment operator for \'foo<int>\'\nstruct foo: crtp<foo<T>> {\nRun Code Online (Sandbox Code Playgroud)\n当编译器实例化 的定义struct foo,并且发现您尚未声明复制赋值运算符时,有义务在右大括号( [ special]/1 ) 之前声明一个隐式 ( [ class.copy.assign]/2 ))。请注意,命名基类复制赋值运算符的using声明不会抑制隐式复制赋值运算符([class.copy.assign]/8 的注释 7)。foo
在声明隐式复制赋值运算符时,编译器需要判断是否删除它。该标准对于何时删除隐式复制赋值运算符有特定的规则( [class.copy.assign]/7 ),并且如果编译器要将其声明为已删除,则必须在第一个声明时执行此操作(如前面提到过,就在右大括号之前)。不允许在没有定义的情况下先声明它,然后再将其定义为已删除([dcl.fct.def.delete]/4)。
\n所以,虽然foo仍然不完整,编译器必须去检查它的复制赋值运算符是否应该被删除。为了做到这一点,它必须检查类类型(或其数组)的每个非静态成员和每个基类是否都有一个可用的复制赋值运算符\xe2\x80\x94,这意味着在这种情况下,它必须执行重载解析确定 crtp<foo>::operator=将调用哪个(给定一个const类型为左值的参数crtp<foo>)。如果重载解析失败,foo\ 的复制赋值运算符将被删除。如果重载解析成功,但所选函数被删除或不可访问,则foo\ 的复制赋值运算符将被删除。
基crtp<foo<int>>类具有用户声明的赋值运算符模板:
template<derived Rhs>\nconstexpr auto operator=(const Rhs&) -> assign<T, Rhs> {\n return {};\n}\nRun Code Online (Sandbox Code Playgroud)\n以及它自己隐式声明的复制和移动赋值运算符。显然,复制赋值运算符应该赢得重载决议,但请参阅[over.match.viable]/1:
\n\n\n从为给定上下文构建的候选函数集中([over.match.funcs]),选择一组可行的函数,通过比较参数转换序列和相关约束([temp.constr .decl]) 以获得最佳拟合 ([over.match.best])。\n可行函数的选择考虑关联约束(如果有)以及自变量和函数参数之间的关系,而不是转换序列的排名。
\n
编译器有义务首先收集可行候选者的列表,然后再对它们进行比较以选择获胜者。为了确定可行的候选人名单,必须首先确定候选人名单。正如[over.match.funcs.general]/8中所指定的,候选函数模板需要进行模板参数推导,以便选择将成为候选的特定专业化。作为推导过程的一部分,编译器必须将推导的模板参数替换为函数类型(任何noexcept-specifier除外),包括其返回类型;请参阅[temp.deduct.general]/7。一般来说,如果此替换失败,则推导失败(并且不会从模板生成候选),但只有直接上下文中的失败才会导致推导失败。
\n在这种情况下,当对返回类型执行替换时,编译器必须检查是否assign<T, Rhs>是有效的type-id。正如[temp.names]/7.5中所指定的,只有满足其约束条件时才有效,因此需要检查Lhs(which is foo<int>) 是否满足概念derived。最后,正是在这一点上发生了硬错误,因为“Derived应该是一个完整的类型”才能使用std::is_base_of1 ( [meta.rel]/2 )。一个类直到其右大括号之后才算完成。
当标准库组件需要完整类型,而用户提供不完整类型时,程序为 IFNDR ( [res.on.functions]/2.5 )。所以所有编译器都是对的。
\n不过,您可能会认为 GCC 不诊断它有点有趣。__is_base_ofClang实际上是在Godbolt链接中使用了libstdc++,并且在实例化内置函数时出现错误。__is_base_of当使用不完整类型实例化时,GCC 是否也会诊断错误?我的实验表明确实如此。那么,即使 GCC 没有义务诊断该程序,您难道不希望它这样做吗?
我怀疑这里发生的事情是 GCC 短路了operator=in的重载解析crtp<foo<int>>:它可以看到复制赋值运算符将获胜,因此它不会为模板化的 执行推导和实例化operator=。在这种特殊情况下,GCC 由 IFNDR 保存,但在其他情况下,GCC 根本不遵循标准,因为在执行所有必需的实例化而需要诊断的情况下,它无法生成诊断并且不短路过载解决。这看起来就是MSVC也短路的情况。尽管这似乎不是 Clang 短路的情况,但我听说还有其他类似的情况。
委员会的一些成员认为[temp.inst]/9(不适用于这种特殊情况,因为我们正在实例化的声明assign<foo<int>,而不是它的定义)应该扩展以允许更多类型的短路,例如在编译器已经知道该函数将失去重载分辨率的情况下拒绝评估约束。
1完整性要求有一些例外情况,您可以通过阅读 [meta.rel]/2 查看。它们不适用于这种情况。
\n