C++20 行为用相等运算符破坏现有代码?

seh*_*ehe 107 c++ spaceship-operator c++17 c++20

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

我一直将其精简为仅使用Boost Operators

  1. 编译器资源管理器C++17 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
    }
    
    Run Code Online (Sandbox Code Playgroud)

    该程序在 GCC 和 Clang 中使用 C++17(启用 ubsan/asan)编译并运行良好。

  2. 当您将隐式构造函数更改为 时explicit,有问题的行显然不再在 C++17 上编译

令人惊讶的是,这两个版本都在 C++20(v1v2)上编译,但它们会导致无法在 C++17 上编译的两行上的无限递归(崩溃或紧密循环,取决于优化级别)。

显然,这种通过升级到 C++20 而潜入的无声错误令人担忧。

问题:

  • 这是否符合 c++20 行为(我希望如此)
  • 究竟是什么干扰?我怀疑这可能是由于 c++20 的新“飞船操作员”支持,但不明白它如何改变这段代码的行为。

Bar*_*rry 83

事实上,不幸的是,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;
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

非逆转候选人比逆转候选人更受欢迎,但只能作为决胜局。更好的转换顺序永远是第一位的。


好的,太好了,我们该如何解决?最简单的选择是完全删除非成员候选人:

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

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

private:
    int t;
};
Run Code Online (Sandbox Code Playgroud)

42 == F{42}这里评估为F{42}.operator==(42),效果很好。

如果我们想保留非成员候选人,我们可以明确添加其反向候选人:

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;
};
Run Code Online (Sandbox Code Playgroud)

这使得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;
};
Run Code Online (Sandbox Code Playgroud)

  • 很棒的步行穿过。我对如何判断情况感到有点困惑。作为一名专业人士,我真的很不高兴看到 C++ 引入兼容性死亡陷阱。我很高兴看到[这种情绪](https://github.com/boostorg/utility/issues/65#issuecomment-636073901),但我担心看起来不会有什么结果。因此,库只是默默地崩溃 - **在运行时** - 这不是一个好的故事情节。我知道我编写的代码会遇到这些问题,并且我不禁想到如果当前的开发人员升级会发生什么。他们可能会义愤填膺地咒骂我的代码 ́\_(ツ)_/̊ (9认同)
  • @Barry我觉得这对于编译器来说静态诊断应该不难(毕竟,这一切都发生在编译时)。即类似“c++20 兼容性警告:将根据 std 模式选择不同的重载,建议修复:...”。 (9认同)
  • @sehe 我对此也很不满意,甚至这也是我的错。这种特定情况是最坏的情况 - 保留 C++17 行为的唯一语言更改是没有该功能的任何部分(或者选择加入“==”功能,但没有人想出一个可以接受的方案)这样做的方法)。它不仅是我们无法真正修复的唯一一个,而且所有其他损坏都是停止编译的代码,而这个却阴险地继续编译。就是各种不好。 (5认同)
  • @sehe 即使在这种情况下,您也可以将 `F` 的主体重写为 `auto operator&lt;=&gt;(F const&amp;) const = default;` 并且这一行为您提供了所有比较。这对于 C++20 的未来发展是有利的,但对于过渡步骤显然不太有利。 (3认同)
  • @Dan 我不是编译器专家,但如果你想尝试在 clang 中添加这样的警告,我相信很多人都会非常感激。 (3认同)