Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where should != operator be defined in a class hierarchy?

Here is a very simple class hierarchy:

class A
{
public:
    A( int _a ) : a( _a ) {}

    virtual bool operator==( const A& right ) const
    {
        return a == right.a;
    }

    virtual bool operator!=( const A& right ) const
    {
        return !( *this == right );
    }

    int a;
};

class B : public A
{
public:
    B( int _a, int _b ) : A( _a ), b( _b ) {}

    virtual bool operator==( const B& right ) const
    {
        return A::operator==( right ) && b == right.b;
    }

    int b;
};

As you can see, operator != is defined in the base class. Because I'm very lazy, I don't want to duplicate such a simple code in all the derived classes.

Unfortunatley, with this code:

A a4(4), a5(5), a4bis(4);
assert( a4 == a4bis );
assert( a4 != a5 );

B b1(4,5), b2(4,6);
assert( !(b1 == b2) );
assert( b1 != b2 ); // fails because B::operator== is not called!

b1 != b2 returns false, because it executes A::operator!= which calls then A::operator== rather than B::operator== (even if the operator is virtual, as derived class version parameter is different, they are not linked in the vtable).

So what's the best way to adress != operator in a generic way for a class hierarchy?

One solution is to repeat it in each class, B would then have:

virtual bool operator!=( const B& right ) const
{
    return !( *this == right );
}

But that's a pain when you have many classes....I have 30....

Another solution would be to have a generic template approach:

template <class T>
bool operator!=( const T& left, const T& right )
{
    return !( left == right );
}

But this bypasses any != operator defined by any class....so it may be risky if one declared it differently (or if one declared a == itself calling !=, it would end up with an infinite loop...). So I feel this solution being very unsafe....except if we can limit the template to be used for all classes derived from the top-level class of our hierarchy (A in my example)....but I don't think this is doable at all.

Note: I'm not using C++11 yet...sorry about that.

like image 231
jpo38 Avatar asked Jul 07 '15 09:07

jpo38


2 Answers

Your function in B...

virtual bool operator==( const B& right ) const

...does not override the function in A...

virtual bool operator==( const A& right ) const

...because the argument types differ. (Differences are only allowed for covariant return types.)

If you correct this, you'll then be able to choose how to compare B objects to other A and A-derived objects, e.g. perhaps:

bool operator==( const A& right ) const override
{
    if (A::operator==( right ))
        if (typeid(*this) == typeid(right))
            return b == static_cast<const B&>(right).b;
    return false;
}

Note that using the typeid comparison above means only B objects will compare equal: any B will compare unequal to any B-derived object. This may or may not be what you want.

With an implementation for B::operator==, the existing != implementation will properly wrap operator==. As Jarod42 observes, your A::operator== isn't robust in that when the left-hand-side value is an A only the A slice of the right-hand-side object will be compared... you might prefer:

virtual bool operator==(const A& right) const
{
    return a == right.a && typeid(*this) == typeid(right);
}

This has the same issues as the B::operator== above: e.g. an A would compare un-equal to a derived object that didn't introduce further data members.

like image 179
Tony Delroy Avatar answered Oct 11 '22 03:10

Tony Delroy


How about something like this ?

class A {
  protected :
    virtual bool equals(const A& right) const {
      return (a == right.a);
    }

  public :
    A(int _a) : a(_a) { }

    bool operator==(const A& right) const {
      return this->equals(right);
    }
    bool operator!=(const A& right) const {
      return !(this->equals(right));
    }

    int a;
};

class B : public A {
  protected :
    virtual bool equals(const A& right) const {
      if (const B* bp = dynamic_cast<const B*>(&right)) {
        return A::equals(right) && (b == bp->b);
      }
      return false;
    }

  public :
    B(int _a, int _b) : A(_a), b(_b) { }

    int b;
};

Move the comparison logic to a separate (virtual) function equals, and call that function from the operator== and operator!= defined in the base class.

The operators don't need to be re-defined in the derived classes. To change the comparison in a derived class, simply override equals.

Note that the dynamic_cast in the code above is used to ensure that the runtime type is a valid type for performing the comparison. Ie. for B::equals, it's used to ensure that right is a B - this is necessary because otherwise right would not have a b member.

like image 43
Sander De Dycker Avatar answered Oct 11 '22 02:10

Sander De Dycker