这是 Clang 的 C++20 概念实现中的错误吗?不必要的约束检查导致无限的模板递归
Is this a bug in Clang's C++20 concepts implementation? Unnecessary checking of constraints causes infinite template recursion
我有一个 C++20 概念的定义:
#include <concepts>
template <typename T> concept Numeric =
requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a - b } -> std::same_as<T>;
{ a * b } -> std::same_as<T>;
{ a / b } -> std::same_as<T>;
};
我有这个 class 模板,它使用概念来约束可以实例化的类型:
(除了倒数第三个之外的大多数方法都有一个最小示例的存根实现)
template <Numeric T>
class Deferred {
public:
Deferred(const Deferred& other) {}
Deferred(T value) {}
Deferred& operator+=(const Deferred& rhs) {
return *this;
}
Deferred& operator/=(const Deferred& rhs) {
return *this;
}
operator T() {
return {};
}
friend Deferred operator/(Deferred lhs, const Deferred& rhs) {
lhs /= rhs;
return lhs;
}
// These last two methods are the ones causing the error
friend Deferred operator+(Deferred lhs, const Deferred& rhs) {
lhs += rhs;
return lhs;
}
template <Numeric TO>
operator TO() {
return {};
}
};
最后,我有了这个使用 Deferred
类的短程序:
int main() {
Deferred<double> result = Deferred<double>(100.0) / Deferred<double>(2.0);
}
此代码在 GCC 11.2 的最新版本上编译良好(demo) and MSVC 19.29 (demo)。
但是在 Clang 12.0.1 上,我收到一个编译器错误,该错误似乎是由检查类型是否满足 Numeric
约束 (demo).[=32= 时触发的无限模板递归引起的]
经过进一步检查,Clang 似乎被提示检查类型 Deferred<double>
是否满足 Numeric
约束,这是由 main()
中的除法触发的出于我不清楚的原因触发 Deferred<double>::operator Deferred<Deferred<double>>
的实例化。
如果删除 Deferred
中的最后两个函数,代码在所有经过测试的编译器上都能正常编译。
有趣的是,如果对 Numeric
中的约束进行重新排序,使得 Deferred
重载的运算符不会作为第一个约束出现(即以+
和 /
) 都不会触发错误。我预计这是由于短路导致在第一次失败时无法评估进一步的约束。
最后,如果 operator TO()
的模板更改为 template <typename TO>
,则不会触发错误。
Clang 在这里检查概念约束是否过于先发制人,或者它的行为是否正确而其他编译器遗漏了什么?
I can't see any reason why it would try to call the generic cast operator for Deferred
, unless mixing const and non-const versions of Deferred
might trigger it in the division operator, or if there is some other implicit conversion here that I am missing.
更新
将转换运算符更改为 explicit operator TO()
会停止 Clang 上的编译器错误,因此看起来隐式转换是错误的原因,但我仍然不确定为什么会这样。
为什么在所有变量类型相同的除法表达式中需要隐式转换?
我认为这是一个 clang 错误。这是一个简化的例子:
template <typename T>
concept Numeric =
requires(T a) {
foo(a);
};
struct Deferred {
friend void foo(Deferred);
template <Numeric TO> operator TO();
};
static_assert(Numeric<Deferred>);
当检查 foo(a)
是否是 Deferred
的有效表达式时,我们必须进行名称查找,找到一个函数,并查看是否可以转换所有参数。在这种情况下,这涉及从 Deferred
类型的左值复制初始化 Deferred
。这应该 只 考虑构造函数,因为我们在这种情况下 [dcl.init.general]/15.6.2:
- Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution ([over.match]). Then: [...]
我们在后续的项目符号中只考虑转换函数:
- Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversions that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution ([over.match]). [...]
这是有道理的 - 这就是构造函数的用途,转换函数是为了获得 不同的 类型(不同的类型)。
但是这里的 clang 无论如何都试图做 a.operator Deferred()
(这显然是无限递归的),这是不正确的。归档 51549.
我有一个 C++20 概念的定义:
#include <concepts>
template <typename T> concept Numeric =
requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a - b } -> std::same_as<T>;
{ a * b } -> std::same_as<T>;
{ a / b } -> std::same_as<T>;
};
我有这个 class 模板,它使用概念来约束可以实例化的类型:
(除了倒数第三个之外的大多数方法都有一个最小示例的存根实现)
template <Numeric T>
class Deferred {
public:
Deferred(const Deferred& other) {}
Deferred(T value) {}
Deferred& operator+=(const Deferred& rhs) {
return *this;
}
Deferred& operator/=(const Deferred& rhs) {
return *this;
}
operator T() {
return {};
}
friend Deferred operator/(Deferred lhs, const Deferred& rhs) {
lhs /= rhs;
return lhs;
}
// These last two methods are the ones causing the error
friend Deferred operator+(Deferred lhs, const Deferred& rhs) {
lhs += rhs;
return lhs;
}
template <Numeric TO>
operator TO() {
return {};
}
};
最后,我有了这个使用 Deferred
类的短程序:
int main() {
Deferred<double> result = Deferred<double>(100.0) / Deferred<double>(2.0);
}
此代码在 GCC 11.2 的最新版本上编译良好(demo) and MSVC 19.29 (demo)。
但是在 Clang 12.0.1 上,我收到一个编译器错误,该错误似乎是由检查类型是否满足 Numeric
约束 (demo).[=32= 时触发的无限模板递归引起的]
经过进一步检查,Clang 似乎被提示检查类型 Deferred<double>
是否满足 Numeric
约束,这是由 main()
中的除法触发的出于我不清楚的原因触发 Deferred<double>::operator Deferred<Deferred<double>>
的实例化。
如果删除 Deferred
中的最后两个函数,代码在所有经过测试的编译器上都能正常编译。
有趣的是,如果对 Numeric
中的约束进行重新排序,使得 Deferred
重载的运算符不会作为第一个约束出现(即以+
和 /
) 都不会触发错误。我预计这是由于短路导致在第一次失败时无法评估进一步的约束。
最后,如果 operator TO()
的模板更改为 template <typename TO>
,则不会触发错误。
Clang 在这里检查概念约束是否过于先发制人,或者它的行为是否正确而其他编译器遗漏了什么?
I can't see any reason why it would try to call the generic cast operator for
Deferred
, unless mixing const and non-const versions ofDeferred
might trigger it in the division operator, or if there is some other implicit conversion here that I am missing.
更新
将转换运算符更改为 explicit operator TO()
会停止 Clang 上的编译器错误,因此看起来隐式转换是错误的原因,但我仍然不确定为什么会这样。
为什么在所有变量类型相同的除法表达式中需要隐式转换?
我认为这是一个 clang 错误。这是一个简化的例子:
template <typename T>
concept Numeric =
requires(T a) {
foo(a);
};
struct Deferred {
friend void foo(Deferred);
template <Numeric TO> operator TO();
};
static_assert(Numeric<Deferred>);
当检查 foo(a)
是否是 Deferred
的有效表达式时,我们必须进行名称查找,找到一个函数,并查看是否可以转换所有参数。在这种情况下,这涉及从 Deferred
类型的左值复制初始化 Deferred
。这应该 只 考虑构造函数,因为我们在这种情况下 [dcl.init.general]/15.6.2:
- Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution ([over.match]). Then: [...]
我们在后续的项目符号中只考虑转换函数:
- Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversions that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution ([over.match]). [...]
这是有道理的 - 这就是构造函数的用途,转换函数是为了获得 不同的 类型(不同的类型)。
但是这里的 clang 无论如何都试图做 a.operator Deferred()
(这显然是无限递归的),这是不正确的。归档 51549.