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

孤街浪徒 提交于 2021-01-20 14:13:48

问题


I ran into this while debugging this question.

I trimmed it down all the way to just using Boost Operators:

  1. Compiler Explorer 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
    }
    

    This program compiles and runs fine with C++17 (ubsan/asan enabled) in both GCC and Clang.

  2. When you change the implicit constructor to explicit, the problematic lines obviously no longer compile on C++17

Surprisingly both versions compile on C++20 (v1 and v2), but they lead to infinite recursion (crash or tight loop, depending on optimization level) on the two lines that wouldn't compile on C++17.

Obviously this kind of silent bug creeping in by upgrading to C++20 is worrisome.

Questions:

  • Is this c++20 behaviour conformant (I expect so)
  • What exactly is interfering? I suspect it might be due to c++20's new "spaceship operator" support, but don't understand how it changes the behaviour of this code.

回答1:


Indeed, C++20 unfortunately makes this code infinitely recursive.

Here's a reduced example:

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

Let's just look at 42 == F{42}.

In C++17, we only had one candidate: the non-member candidate (#2), so we select that. Its body, x == y, itself only has one candidate: the member candidate (#1) which involves implicitly converting y into an F. And then that member candidate compares the two integer members and this is totally fine.

In C++20, the initial expression 42 == F{42} now has two candidates: both the non-member candidate (#2) as before and now also the reversed member candidate (#1 reversed). #2 is the better match - we exactly match both arguments instead of invoking a conversion, so it's selected.

Now, however, x == y now has two candidates: the member candidate again (#1), but also the reversed non-member candidate (#2 reversed). #2 is the better match again for the same reason that it was a better match before: no conversions necessary. So we evaluate y == x instead. Infinite recursion.

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


Okay great, how can we fix it? The simplest option is removing the non-member candidate entirely:

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

This makes 42 == F{42} still choose the non-member candidate, but now x == y in the body there will prefer the member candidate, which then does the normal equality.

This last version can also remove the non-member candidate. The following also works without recursion for all test cases (and is how I would write comparisons in C++20 going forward):

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


来源:https://stackoverflow.com/questions/65648897/c20-behaviour-breaking-existing-code-with-equality-operator

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!