2

I have implemented two classes (parent and derived) comparison operator, but testing it I have noticed a confusing behavior in case of using pointers. In addition, I have some other questions about good practices.

Here it is the code:

struct A { int getN() { return _n; } virtual bool equals(A &other) { return getN() == other.getN(); } friend bool operator==(const A &one, const A &other); A(int n) : _n(n) { } private: int _n; }; bool operator==(const A &one, const A &other) { return one._n == other._n; } struct B : public A { friend bool operator==(const B &one, const B &other); B(int n, int m = 0) : A(n), _m(m) { } private: int _m; }; bool operator==(const B &one, const B &other) { if( operator==(static_cast<const A&>(one), static_cast<const A&>(other)) ){ return one._m == other._m; } else { return false; } } int main() { A a(10), a2(10); B b(10, 20), b2(10, 20), b3(10, 30); A *a3 = new B(10, 20); bool x1 = a == a2; // calls A operator (1) bool x2 = b == b2; // calls B operator (2) bool x3 = a == b; // calls A operator (3) bool x4 = b == a; // calls A operator (4) bool x5 = b == *a3; // calls A operator (5) bool x6 = a == b3; // calls A operator (6) bool x7 = b3 == a; // calls A operator (7) return 0; } 

Questions

Comparing A instances with B ones, the A class operator is called, is this the correct behavior?

The point 5 is the one that seems confusing to me. a3 is declared as A but instanced as B, but the class A operator is called. Is there any way to solve this?

If the operator was implemented as a instance method, depending on it is called with an A object or a B one, the executed method is different. For example:

a == b // executes A::operator== b == a // executes B::operator== 

I assume that this is confusing and error prone, and must be avoided. Am I right?

4
  • Yes, const B& is implicitly convertible to const A&, and the rest is just usual overload resolution. Also b == a // executes B::operator==, really? I'd expect it not to compile for lack of a suitable overload in B (A::operator== being hidden) or to call A::operator== if there's using A::operator== in B Commented Mar 6, 2018 at 17:51
  • Notice that the last part of the post describes the behavior incase of operator== was a member function, so B::operator==(const A& other) would be called in that case. Commented Mar 6, 2018 at 18:08
  • Well, sure, if you define it. But why wouldn't you define the free function bool operator==(const B&, const A&) then? Commented Mar 6, 2018 at 18:16
  • I was only want to confirm that one of the reasons for implementing operator== as a free function is to avoid the problem about the operands' order. Commented Mar 6, 2018 at 19:10

3 Answers 3

2

Comparing A instances with B ones, the A class operator is called, is this the correct behavior?

Yes, because this is the only applicable overload.

If the operator was implemented as a instance method, depending on it is called with an A object or a B one, the executed method is different. [...] I assume that this is confusing and error prone, and must be avoided.

Right, this is not a good idea, because equality operator must be symmetric. Although it is possible to implement it symmetrically with two separate operators, it introduces a maintenance liability in the code.

One approach to solve this is to expan your equals member function, and have each subclass implement equality for its own type:

struct A { int getN() const { return _n; } virtual bool equals(A &other) const { return getN() == other.getN(); } friend bool operator==(const A &one, const A &other); A(int n) : _n(n) { } private: int _n; }; struct B : public A { B(int n, int m = 0) : A(n), _m(m) { } virtual bool equals(B &other) const { return A::equals(*this, other) && _m == other._m; } private: int _m; }; bool operator==(const A &one, const A &other) { return typeid(one)==typeid(two) && one.equals(two); } 

"At the heart" of this implementation is typeid operator, which lets you check for type equality at run-time. The virtual call to one.equals(two) happens only if the types of one and two are exactly the same.

This approach puts responsibility for equality comparison with the class itself. In other words, each class needs to know how to compare its own instances, and optionally rely on its base for comparison of its state.

Sign up to request clarification or add additional context in comments.

Comments

0

Comparing A instances with B ones, the A class operator is called, is this the correct behavior?

B& can be converted to const A& but A& can't be converted to const B&. So comparing a == b there's really only one choice.

"Class operators" isn't a thing. In your code you've implemented operator== as non-member functions, that you've also made friends of A and B.

The point 5 is the one that seems confusing to me. a3 is declared as A but instanced as B, but the class A operator is called. Is there any way to solve this?

Dereferencing a3 returns A&, not B&.

If the operator was implemented as a instance method, depending on it is called with an A object or a B one, the executed method is different. For example:

a == b // executes A::operator== b == a // executes B::operator== 

I assume that this is confusing and error prone, and must be avoided. Am I right?

If operator== is implemented as an instance method it's called on the left hand operand. In the first case it's a, in the second it's b. So b == a is only valid if you've implemented bool B::operator==(const A&).

3 Comments

"When you dereference a3 it gets sliced." no it is not
Thank you @Slava, I corrected my answer to the best of my understanding.
"Class operators" isn't a thing.": Yes, of course @ryhp . I was trying to refer one or the other implementation in a fast way, sorry if I have been confusing or incorrect.
0

Comparing A instances with B ones, the A class operator is called, is this the correct behavior?

Since you do not have any virtual functions in A you only deal with function overloading which means that compiler decides which function to call at compile time. As B publicly inherited from A, pointer or reference to B can be implicitly converted to A but not vice versa. In this case it means "unless both arguments are B only first resolution would be called". Note when you dereference a pointer only type of pointer matters to determine type at compile time, so if A *pointer points to instance of A or B does not matter in this case at all, *pointer always has type A.

If you want functions to be called based on actual type you need to use virtual functions, details on how to do that for operator== can be found here implementing operator== when using inheritance

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.