这是 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.