Search code examples
c++language-lawyereigeneigen3c++20

Breaking change in C++20 or regression in clang-trunk/gcc-trunk when overloading equality comparison with non-Boolean return value?


The following code compiles fine with clang-trunk in c++17 mode but breaks in c++2a (upcoming c++20) mode:

// Meta struct describing the result of a comparison
struct Meta {};

struct Foo {
    Meta operator==(const Foo&) { return Meta{}; }
    Meta operator!=(const Foo&) { return Meta{}; }
};

int main()
{
    Meta res = (Foo{} != Foo{});
}

It also compiles fine with gcc-trunk or clang-9.0.0: https://godbolt.org/z/8GGT78

The error with clang-trunk and -std=c++2a:

<source>:12:19: error: use of overloaded operator '!=' is ambiguous (with operand types 'Foo' and 'Foo')
    Meta res = (f != g);
                ~ ^  ~
<source>:6:10: note: candidate function
    Meta operator!=(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function
    Meta operator==(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function (with reversed parameter order)

I understand that C++20 will make it possible to only overload operator== and the compiler will automatically generate operator!= by negating the result of operator==. As far as I understand, this only works as long as the return type is bool.

The source of the problem is that in Eigen we declare a set of operators ==, !=, <, ... between Array objects or Array and Scalars, which return (an expression of) an Array of bool (which can then be accessed element-wise, or used otherwise). E.g.,

#include <Eigen/Core>
int main()
{
    Eigen::ArrayXd a(10);
    a.setRandom();
    return (a != 0.0).any();
}

In contrast to my example above this even fails with gcc-trunk: https://godbolt.org/z/RWktKs. I have not managed yet to reduce this to a non-Eigen example, which fails in both clang-trunk and gcc-trunk (the example at the top is quite simplified).

Related issue report: https://gitlab.com/libeigen/eigen/issues/1833

My actual question: Is this actually a breaking change in C++20 (and is there a possibility to overload the comparison operators to return Meta-objects), or is it more likely a regression in clang/gcc?


Solution

  • The Eigen issue appears to reduce to the following:

    using Scalar = double;
    
    template<class Derived>
    struct Base {
        friend inline int operator==(const Scalar&, const Derived&) { return 1; }
        int operator!=(const Scalar&) const;
    };
    
    struct X : Base<X> {};
    
    int main() {
        X{} != 0.0;
    }
    

    The two candidates for the expression are

    1. the rewritten candidate from operator==(const Scalar&, const Derived&)
    2. Base<X>::operator!=(const Scalar&) const

    Per [over.match.funcs]/4, as operator!= was not imported into the scope of X by a using-declaration, the type of the implicit object parameter for #2 is const Base<X>&. As a result, #1 has a better implicit conversion sequence for that argument (exact match, rather than derived-to-base conversion). Selecting #1 then renders the program ill-formed.

    Possible fixes:

    • Add using Base::operator!=; to Derived, or
    • Change the operator== to take a const Base& instead of a const Derived&.