C++20 behaviour breaking existing code with equality operator?

我在调试 这个问题时遇到了这个问题。

我把它简化到只用 Boost Operators:

  1. 编译器资源管理器 C + + 17 < a href = “ https://Godbolt.org/z/szYx9E”rel = “ noReferrer”> C + + 20

    #include <boost/operators.hpp>
    
    
    struct F : boost::totally_ordered1<F, boost::totally_ordered2<F, int>> {
    /*implicit*/ F(int t_) : t(t_) {}
    bool operator==(F const& o) const { return t == o.t; }
    bool operator< (F const& o) const { return t <  o.t; }
    private: int t;
    };
    
    
    int main() {
    #pragma GCC diagnostic ignored "-Wunused"
    F { 42 } == F{ 42 }; // OKAY
    42 == F{42};         // C++17 OK, C++20 infinite recursion
    F { 42 } == 42;      // C++17 OK, C++20 infinite recursion
    }
    

    这个程序在 GCC 和 Clang 中使用 C + + 17(启用了 ubsan/asan)编译和运行得很好。

  2. 当您将 含蓄构造函数更改为 explicit时,问题行显然 < strong > < a href = “ https://Godbolt.org/z/hq4fve”rel = “ noReferrer”> 不再在 C + + 17上编译

令人惊讶的是,这两个版本的 在 C + + 20上编译(ref = “ https://Godbolt.org/z/onMzo5”rel = “ noReferrer”> v1 和 < a href = “ https://Godbolt.org/z/b1Pav5”rel = “ noReferrer”> v2 ),但他们导致 无限递归(崩溃或紧密循环,取决于优化水平)的两行,不会在 C + + 17上编译。

很明显,这种通过升级到 C + + 20而悄然出现的无声 bug 令人担忧。

问题:

  • 这个 c + + 20行为是否符合(我希望如此)
  • 到底是什么干扰?我怀疑这可能是由于 c + + 20新的“宇宙飞船操作员”的支持,但不理解 怎么做它改变了这个代码的行为。
5339 次浏览

事实上,C + + 20不幸地使这段代码无限递归。

下面是一个简化的例子:

struct F {
/*implicit*/ F(int t_) : t(t_) {}


// member: #1
bool operator==(F const& o) const { return t == o.t; }


// non-member: #2
friend bool operator==(const int& y, const F& x) { return x == y; }


private:
int t;
};

让我们看看 42 == F{42}

在 C + + 17中,我们只有一个候选者: 非成员候选者(#2) ,所以我们选择它。它的主体 x == y本身只有一个候选者: 成员候选者(#1) ,它涉及到将 y隐式转换为 F。然后候选成员比较两个整数成员,这完全没问题。

在 C + + 20中,初始表达式 42 == F{42}现在有 候选者: 非成员候选者(#2)和以前一样,现在还有反向成员候选者(#1反向)。#2是更好的匹配-我们完全匹配两个参数,而不是调用转换,所以它被选中。

现在,但是,x == y现在有 候选人: 成员候选人再次(#1) ,但也反过来的非成员候选人(#2反过来)。#2再次成为更好的匹配,原因与之前的匹配相同: 不需要转换。所以我们改为评估 y == x。无限递归。

Non-reversed candidates are preferred to reversed candidates, but only as a tiebreaker. Better conversion sequence is always first.


很好,我们怎么解决这个问题? 最简单的方法就是把非会员候选人完全移除:

struct F {
/*implicit*/ F(int t_) : t(t_) {}


bool operator==(F const& o) const { return t == o.t; }


private:
int t;
};

42 == F{42} here evaluates as F{42}.operator==(42), which works fine.

If we want to keep the non-member candidate, we can add its reversed candidate explicitly:

struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
bool operator==(int i) const { return t == i; }
friend bool operator==(const int& y, const F& x) { return x == y; }


private:
int t;
};

这使得 42 == F{42}仍然选择非成员候选者,但是现在 x == y在主体中会更喜欢成员候选者,然后执行正常的相等操作。

最后一个版本也可以删除非成员候选者。下面的代码对于所有的测试用例来说也是不需要递归的(这也是我将来在 C + + 20中编写比较的方法) :

struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
bool operator==(int i) const { return t == i; }


private:
int t;
};