【问题标题】:C++20 behaviour breaking existing code with equality operator?C++20 行为用相等运算符破坏现有代码?
【发布时间】:2021-04-15 08:03:03
【问题描述】:

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

我一直把它删减到只使用Boost Operators

  1. 编译器资源管理器C++17C++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时,问题行明显no longer compile on C++17

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

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

问题:

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

【问题讨论】:

标签: c++ c++17 c++20 spaceship-operator


【解决方案1】:

确实,不幸的是,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),现在也是反向成员候选者 (@987654331 @反转)。 #2 是更好的匹配 - 我们完全匹配两个参数而不是调用转换,所以它被选中。

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

未反转的候选人比反转的候选人更受青睐,但仅作为决胜局。更好的转化顺序永远是第一位的。


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

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

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

private:
    int t;
};

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;
};

这使得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;
};

【讨论】:

  • 出色的演练。我对如何判断情况感到有些不安。作为一名专业人士,我真的很不高兴看到 C++ 引入了兼容性死亡陷阱。我很高兴看到this sentiment,但我担心它看起来没什么大不了的。所以库只是默默地中断 - 在运行时 - 这不是一个好的故事情节。我知道我编写的代码会遇到这些问题,我不禁想到如果当前的开发人员升级会发生什么。他们可能会义愤填膺地诅咒我的代码¯_(ツ)_/¯
  • @sehe 我也很不高兴,这也是我的错。而这种特殊情况是最坏的情况 - 保留 C++17 行为的唯一语言更改是没有任何功能部分(或选择加入 == 功能,没有人想出一个可行的方法这样做)。它不仅是唯一一个我们无法真正修复的问题,而且所有其他漏洞都是停止编译的代码,而这个漏洞仍在继续编译。只是各种各样的坏事。
  • @sehe 即使在这种情况下,您也可以将F 的主体重写为auto operator&lt;=&gt;(F const&amp;) const = default;,然后一行就可以进行所有比较。这对于 C++20 的发展是有好处的,但对于过渡步骤显然不是那么好。
  • @Barry 我觉得编译器静态诊断应该不难(毕竟,这一切都发生在编译时)。 IE。类似“c++20 兼容警告:将根据标准模式选择不同的重载,建议修复:...”。
  • @Dan 我不是编译器,但如果你想尝试在 clang 中添加这样的警告,我相信很多人会非常感激。
猜你喜欢
  • 2021-05-11
  • 2021-08-08
  • 2013-01-24
  • 1970-01-01
  • 1970-01-01
  • 2021-05-03
  • 2015-04-02
  • 2020-11-21
  • 2021-09-01
相关资源
最近更新 更多