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

后端 未结 1 1508
無奈伤痛
無奈伤痛 2021-01-30 05:12

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

相关标签:
1条回答
  • 2021-01-30 05:45

    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;
    };
    
    0 讨论(0)
提交回复
热议问题