c++language-lawyerc++20c++-conceptscomparison-operators

The presence of both operator == and operator != breaks some concepts


After upgrading to latest Visual Studio 2022 version 17.6, one of our custom views stopped to be recognized as std::ranges::range. It turned out that the problem was in the presence of both operator == and operator != in the view's iterator.

Please find below minimal simplified example (without views and iterators already):

struct A {
    friend bool operator ==( const A &, const A & ) = default;
};

struct B {
    friend bool operator ==( const B &, const B & ) = default;
    friend bool operator ==( const B &, const A & ) { return false; }
    // Visual Studio 2022 version 17.6 does not like next line
    friend bool operator !=( const B &, const A & ) { return true; }
};

template< class T, class U >
concept comparable =
  requires(const std::remove_reference_t<T>& t,
           const std::remove_reference_t<U>& u) {
    { t == u } -> std::same_as<bool>;
    { t != u } -> std::same_as<bool>;
    { u == t } -> std::same_as<bool>;
    { u != t } -> std::same_as<bool>;
  };
  
// ok in GCC, Clang and Visual Studio before version 17.6
static_assert( comparable<A, B> );

The example is accepted by GCC and Clang, but not the latest Visual Studio, which prints the error:

<source>(25): error C2607: static assertion failed
<source>(25): note: the concept 'comparable<A,B>' evaluated to false
<source>(18): note: 'bool operator ==(const B &,const A &)': rewritten candidate function was excluded from overload resolution because a corresponding operator!= declared in the same scope
<source>(4): note: could be 'bool operator ==(const A &,const A &)' [found using argument-dependent lookup]
<source>(8): note: or       'bool operator ==(const B &,const B &)' [found using argument-dependent lookup]
<source>(9): note: or       'bool operator ==(const B &,const A &)' [found using argument-dependent lookup]
<source>(4): note: or 'bool operator ==(const A &,const A &)' [synthesized expression 'y == x']
<source>(8): note: or 'bool operator ==(const B &,const B &)' [synthesized expression 'y == x']
<source>(9): note: or 'bool operator ==(const B &,const A &)' [synthesized expression 'y == x']
<source>(18): note: 'bool operator ==(const B &,const A &)': rewritten candidate function was excluded from overload resolution because a corresponding operator!= declared in the same scope
<source>(18): note: while trying to match the argument list '(const A, const B)'

Online demo: https://gcc.godbolt.org/z/evTfofq3d

Is it a bug in new Visual Studio compiler, or on the contrary the others are wrong?


Solution

  • This is a result of The Equality Operator You Are Looking For (P2468).

    The comparison changes in C++20 - which allow a == b and a != b to find rewritten and synthesized candidates - can break a lot of C++17 code. You can see some examples in the paper.

    In an effort to mitigate those breaks (not all, but at least most), Cameron DaCamara, an MSVC compiler developer, came up with a narrow rule that attempts to use the C++17 comparison rules for C++17-ish code. Basically, in C++20, there's no need to write operator!= since the existing operator== will be good enough (a != b can use the rewritten expression !(a == b), which is almost always what you want, so why bother writing that?). But in C++17, we didn't have this rule yet, so you had to write both.

    Thus, the proposed rule is as follows: if we find an operator== and an operator!= in the same scope with the same parameters, use the C++17 rules. Otherwise, use the C++20 rules. This is one of those C++ rules that I kind of hoped that nobody needed to know about - your C++17 code just continues to work fine, and your new C++20 code would just work to begin with.


    Using the posted reduced example:

    struct A { };
    
    struct B {
      friend bool operator==(const B &, const A &);
      friend bool operator!=(const B &, const A &);
    };
    
    int main() {
      return A{} == B{};
    }
    

    In C++17, this doesn't compile, because there's no viable operator.

    In C++20, before the paper I'm talking about, this would evaluate as B{} == A{}. But in C++20, with the resolution of this DR, because you're providing both operator== and operator!= and they're the same, we assume that you wanted the C++17 rules, so we're back to this not compiling, because we don't consider the rewritten candidate.

    The fix is simply to not provide that operator:

    struct A { };
    
    struct B {
      friend bool operator==(const B &, const A &);
    };
    
    int main() {
      return A{} == B{};
    }
    

    Or, if you need to handle both C++17/C++20, then that one isn't enough anyway, since you need to provide three others - all conditionally:

    struct A { };
    
    struct B {
      friend bool operator==(const B&, const A&);
    #if !(defined(__cpp_impl_three_way_comparison) and __cpp_impl_three_way_comparison >= 201907)
      friend bool operator!=(const B& b, const A& a) { return !(b == a); }
      friend bool operator==(const A& a, const B& b) { return b == a; }
      friend bool operator!=(const A& a, const B& b) { return !(b == a); }
    #endif
    };
    
    int main() {
      return A{} == B{};
    }
    

    As an update, this is CWG 2804, which points out that the specific wording from P2468 didn't quite cover the intent correctly and doesn't handle friend operators in the way you might expect. From the issue:

    struct X {
      operator int();
      friend bool operator==(X, int);
      friend bool operator!=(X, int);  // #1
    } x;
    
    bool bx = x == x;    // error: lookup for rewrite target determination does not find hidden friend #1
    
    struct Y {
      operator int();
      friend bool operator==(Y, int);   // #2
    } y;
    
    bool operator!=(Y, int);            // #3
    
    bool by = y == y;                   // OK, #2 is not a rewrite target because lookup finds #3
    

    Arguably the intent is that bx should be ok (we should not consider rewrites in this situation because operator== and operator!= were declared together) and by should be an error (because we should consider rewrites in this case, and consider rewrites leads to ambiguity).


    Technically there are 9 other authors listed on this paper besides Cameron, including me, but Cameron did the bulk of the work here, with him and Richard Smith coming up with the final rule set. To the extent that I had meaningful involvement in the paper, it was that I was the one who broke the code that necessitated the fix to begin with. So given that Cameron came up with this design, and tested it against a lot of existing code, it's not surprising that MSVC is the first compiler to implement the new rule.