Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Comparison Operator Overloading const vs non-const Behaviour

I recently noticed some operator overloading behaviour that I could not figure out myself. The following two classes only differ in the const on the member comparison operator overloads of ClassA. In ClassB they are not const. Generally I know that one would always prefer the const one, but still I am interested in why we see the behaviour I will describe below.

#include <string>

class ClassA {
public:
    explicit ClassA(double t) : _t(t) {}

    std::string operator<=(int const& other) const {
        return "A(<=)";
    }

    std::string operator==(int const& other) const {
        return "A(==)";
    }

    friend std::string operator<=(int const& other, ClassA const& expr) {
        return "A'(<=)";
    }

    friend std::string operator==(int const& other, ClassA const& expr) {
        return "A'(==)";
    }

private:
    double _t;
};

class ClassB {
public:
    explicit ClassB(double t) : _t(t) {}

    std::string operator<=(int const& other) {
        return "B(<=)";
    }

    std::string operator==(int const& other) {
        return "B(==)";
    }

    friend std::string operator<=(int const& other, ClassB const& expr) {
        return "B'(<=)";
    }

    friend std::string operator==(int const& other, ClassB const& expr) {
        return "B'(==)";
    }

private:
    double _t;
};

Now I want to use these classes and comparison functions in const and non-const scenarios.

int
main(int argc,
     char* argv[]) {
    ClassA a1{0};
    1==a1; //OK
    1<=a1; //OK
    ClassA const a2{0};
    1==a2; //OK
    1<=a2; //OK
    ClassB b1{0};
    1==b1; //NOT OK
    1<=b1; //OK
    ClassB const b2{0};
    1==b2; //OK
    1<=b2; //OK

    return 0;
}

Everything works fine but the one line that I marked NOT OK. This throws a compiler error.

error C2446: '==': no conversion from 'ClassB' to 'int'

My question splits up into three parts, but I would expect that there is one good reason that answers all of them. So I hope it is still fine to post this into one single SO question.

Why is the equality operator == not compiling, when the inequality <= is? Why does it matter for the friend functions whether the member functions are const or not? And why does making the ClassB object const fix it?

Updates:

  • In the comments @Eljay pointed out that the problem is probably created by the a new C++20 feature that automatically generates comparison operators with inverted arguments. This apparently makes member std::string operator==(int const& other) (after re-arrangement) a better match for the 1==b1. After some digging I found the rule saying these should be generated in the rules for overload resolution.
  1. rewritten candidates:
  • For the four relational operator expressions x<y, x<=y, x>y, and x>=y, all member, non-member, and built-in operator<=>'s found are added to the set.
  • For the four relational operator expressions x<y, x<=y, x>y, and x>=y as well as the three-way comparison expression x<=>y, a synthesized candidate with the order of the two parameters reversed is added for each member, non-member, and built-in operator<=>'s found.
  • For x!=y, all member, non-member, and built-in operator=='s found are added to the set.
  • For equality operator expressions x==y and x!=y, a synthesized candidate with the order of the two parameters reversed is added for each member, non-member, and built-in operator=='s found.

In all cases, rewritten candidates are not considered in the context of the rewritten expression. For all other operators, the rewritten candidate set is empty.

  • @463035818_is_not_a_number pointed out some interesting finding about different compilers that can and can not compile the code in different versions. Specifically for clang and gcc with the flag -std=c++2a the newest versions x86-64 clang 12.0.0 and x86-64 gcc 11.1 do not compile, while older versions x86-64 clang 9.0.1 and x86-64 gcc 9.4 do. For VisualStudio we see a similar pattern with the flag /std:c++latest. Here the newest version x64 msvc v19.28 (VS16.9) does not compile, where the direct predecessor x64 msvc v19.28 does. These test where made with the Compiler explorer godbolt.org.

  • It is particularly interesting to note that the compiler errors of clang and gcc suggest that the problem is that std::string operator==(int const& other) is not returning a bool.

clang

error: return type 'std::string' (aka 'basic_string<char>') of selected 'operator==' function for rewritten '==' comparison is not 'bool'
    1==b1; //NOT OK

gcc

error: return type of 'std::string ClassB::operator==(const int&)' is not 'bool'
    1==b1; //NOT OK

While these are all very interesting insights the original question still remains open.

like image 947
SimonT Avatar asked Nov 06 '22 01:11

SimonT


1 Answers

Not a concrete answer. But still, let's look into documentation.

There are no restrictions on return types for overloaded operators (since return type does not participate in overload resolution), but there are canonical implementations:

...the language puts no other constraints on what the overloaded operators do, or on the return type, but in general, overloaded operators are expected to behave as similar as possible to the built-in operators

And then:

..the return types are limited by the expressions in which the operator is expected to be used.

For example, assignment operators return by reference to make it possible to write a = b = c = d, because the built-in operators allow that.

We dig further:

...where built-in operators return bool, most user-defined overloads also return bool so that the user-defined operators can be used in the same manner as the built-ins. However, in a user-defined operator overload, any type can be used as return type (including void).

And even further (three-way comparison):

If both operands have arithmetic types, or if one operand has unscoped enumeration type and the other has integral type, the usual arithmetic conversions are applied to the operands.

So, I would assert that it depends on an implementation. On my machine it compiles (g++) and runs:

std::cout << (1==b1) << std::endl; // Prints B'(==)

Tiny re-update

@463035818_is_not_a_number: "The issue was seen in VS. Newer versions of gcc reject such usage as well, same with clang. It rather looks like it was a bug / missing feature and got fixed in more recent versions."

Here's the compiler explorer snippet with the issue.

like image 86
rawrex Avatar answered Nov 12 '22 21:11

rawrex