78

Suppose I have the following class hierarchy:

class A
{
    int foo;
    virtual ~A() = 0;
};

A::~A() {}

class B : public A
{
    int bar;
};

class C : public A
{
    int baz;
};

What's the right way to overload operator== for these classes? If I make them all free functions, then B and C can't leverage A's version without casting. It would also prevent someone from doing a deep comparison having only references to A. If I make them virtual member functions, then a derived version might look like this:

bool B::operator==(const A& rhs) const
{
    const B* ptr = dynamic_cast<const B*>(&rhs);        
    if (ptr != 0) {
        return (bar == ptr->bar) && (A::operator==(*this, rhs));
    }
    else {
        return false;
    }
}

Again, I still have to cast (and it feels wrong). Is there a preferred way to do this?

Update:

There are only two answers so far, but it looks like the right way is analogous to the assignment operator:

  • Make non-leaf classes abstract
  • Protected non-virtual in the non-leaf classes
  • Public non-virtual in the leaf classes

Any user attempt to compare two objects of different types will not compile because the base function is protected, and the leaf classes can leverage the parent's version to compare that part of the data.

Michael Kristofik
  • 34,290
  • 15
  • 75
  • 125
  • 1
    This is a classical double dispatch problem. Either your hierarchy is known ahead of time, in which case you must write n * (n - 1) / 2 functions, or it is not and you must find another way (eg. return a hash of the object and compare hashes). – Alexandre C. Feb 01 '13 at 19:06

5 Answers5

34

For this sort of hierarchy I would definitely follow the Scott Meyer's Effective C++ advice and avoid having any concrete base classes. You appear to be doing this in any case.

I would implement operator== as a free functions, probably friends, only for the concrete leaf-node class types.

If the base class has to have data members, then I would provide a (probably protected) non-virtual helper function in the base class (isEqual, say) which the derived classes' operator== could use.

E.g.

bool operator==(const B& lhs, const B& rhs)
{
    return lhs.isEqual( rhs ) && lhs.bar == rhs.bar;
}

By avoiding having an operator== that works on abstract base classes and keeping compare functions protected, you don't ever get accidentally fallbacks in client code where only the base part of two differently typed objects are compared.

I'm not sure whether I'd implement a virtual compare function with a dynamic_cast, I would be reluctant to do this but if there was a proven need for it I would probably go with a pure virtual function in the base class (not operator==) which was then overriden in the concrete derived classes as something like this, using the operator== for the derived class.

bool B::pubIsEqual( const A& rhs ) const
{
    const B* b = dynamic_cast< const B* >( &rhs );
    return b != NULL && *this == *b;
}
azalea
  • 11,402
  • 3
  • 35
  • 46
CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • 8
    You definitively need the operator== in the abstract class in order to grant polymorphism. I don't think this answer is good because it does not solve the problem. – fachexot Jul 01 '14 at 20:50
  • 1
    In general I think the base class should define a operator== overload (internally or via friend class doesn't matter) which check typeid equality and calls an abstract virtual "equals" function which the derived class will define. In that function the derived class could even use static_cast because the typeid is already been checked to be the same. The advantage is that the user, which should typically only use the interface, can use the more straightforward == to compare two objects rather than having to call a custom function – Triskeldeian May 01 '17 at 10:30
19

If you dont want to use casting and also make sure you will not by accident compare instance of B with instance of C then you need to restructure your class hierarchy in a way as Scott Meyers suggests in item 33 of More Effective C++. Actually this item deals with assignment operator, which really makes no sense if used for non related types. In case of compare operation it kind of makes sense to return false when comparing instance of B with C.

Below is sample code which uses RTTI, and does not divide class hierarchy into concreate leafs and abstract base.

The good thing about this sample code is that you will not get std::bad_cast when comparing non related instances (like B with C). Still, the compiler will allow you to do it which might be desired, you could implement in the same manner operator< and use it for sorting a vector of various A, B and C instances.

live

#include <iostream>
#include <string>
#include <typeinfo>
#include <vector>
#include <cassert>

class A {
    int val1;
public:
    A(int v) : val1(v) {}
protected:
    friend bool operator==(const A&, const A&);
    virtual bool isEqual(const A& obj) const { return obj.val1 == val1; }
};

bool operator==(const A& lhs, const A& rhs) {
    return typeid(lhs) == typeid(rhs) // Allow compare only instances of the same dynamic type
           && lhs.isEqual(rhs);       // If types are the same then do the comparision.
}

class B : public A {
    int val2;
public:
    B(int v) : A(v), val2(v) {}
    B(int v, int v2) : A(v2), val2(v) {}
protected:
    virtual bool isEqual(const A& obj) const override {
        auto v = dynamic_cast<const B&>(obj); // will never throw as isEqual is called only when
                                              // (typeid(lhs) == typeid(rhs)) is true.
        return A::isEqual(v) && v.val2 == val2;
    }
};

class C : public A {
    int val3;
public:
    C(int v) : A(v), val3(v) {}
protected:
    virtual bool isEqual(const A& obj) const override {
        auto v = dynamic_cast<const C&>(obj);
        return A::isEqual(v) && v.val3 == val3;
    }
};

int main()
{
    // Some examples for equality testing
    A* p1 = new B(10);
    A* p2 = new B(10);
    assert(*p1 == *p2);

    A* p3 = new B(10, 11);
    assert(!(*p1 == *p3));

    A* p4 = new B(11);
    assert(!(*p1 == *p4));

    A* p5 = new C(11);
    assert(!(*p4 == *p5));
}
marcinj
  • 48,511
  • 9
  • 79
  • 100
  • 7
    You should use static_cast instead of dynamic_cast. As you already checked the typeid, this is safe, and faster. – galinette Oct 05 '16 at 21:36
  • https://godbolt.org/z/7fx7fd9Gv What if the copy/move constructors need to be marked as deleted in base class? Derived class want to implement a totally abstract base class with pure virtual functions. – cpchung Mar 28 '22 at 22:09
16

I was having the same problem the other day and I came up with the following solution:

struct A
{
    int foo;
    A(int prop) : foo(prop) {}
    virtual ~A() {}
    virtual bool operator==(const A& other) const
    {
        if (typeid(*this) != typeid(other))
            return false;

        return foo == other.foo;
    }
};

struct B : A
{
    int bar;
    B(int prop) : A(1), bar(prop) {}
    bool operator==(const A& other) const
    {
        if (!A::operator==(other))
            return false;

        return bar == static_cast<const B&>(other).bar;
    }
};

struct C : A
{
    int baz;
    C(int prop) : A(1), baz(prop) {}
    bool operator==(const A& other) const
    {
        if (!A::operator==(other))
            return false;

        return baz == static_cast<const C&>(other).baz;
    }
};

The thing I don't like about this is the typeid check. What do you think about it?

mtvec
  • 17,846
  • 5
  • 52
  • 83
  • I think you'll get more help posting this as a separate question. Also, you should consider Konrad Rudolph's answer and think about whether you really need to use `operator==` in this way. – Michael Kristofik Jan 11 '10 at 14:49
  • 1
    A question about Konrad Rudolph's post: what's the difference between a virtual equals method and a virtual operator==? AFAIK, operators are just normal methods with a special notation. – mtvec Jan 12 '10 at 08:57
  • 1
    @Job: they are. But an implicit expectation is that operators do not perform virtual operations, if I recall correctly what Scott Meyers had to say in Effective C++. To be fair though, I’m not sure any more and I don’t have the book handy just now. – Konrad Rudolph Jan 12 '10 at 12:18
  • There are cpp guidelines that recommend to avoid virtual bool operator==() (see [here C-87](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#c87-beware-of--on-base-classes)) –  Dec 17 '21 at 10:31
9

If you make the reasonable assumption that the types of both objects must be identical for them to be equal, there's a way to reduce the amount of boiler-plate required in each derived class. This follows Herb Sutter's recommendation to keep virtual methods protected and hidden behind a public interface. The curiously recurring template pattern (CRTP) is used to implement the boilerplate code in the equals method so the derived classes don't need to.

class A
{
public:
    bool operator==(const A& a) const
    {
        return equals(a);
    }
protected:
    virtual bool equals(const A& a) const = 0;
};

template<class T>
class A_ : public A
{
protected:
    virtual bool equals(const A& a) const
    {
        const T* other = dynamic_cast<const T*>(&a);
        return other != nullptr && static_cast<const T&>(*this) == *other;
    }
private:
    bool operator==(const A_& a) const  // force derived classes to implement their own operator==
    {
        return false;
    }
};

class B : public A_<B>
{
public:
    B(int i) : id(i) {}
    bool operator==(const B& other) const
    {
        return id == other.id;
    }
private:
    int id;
};

class C : public A_<C>
{
public:
    C(int i) : identity(i) {}
    bool operator==(const C& other) const
    {
        return identity == other.identity;
    }
private:
    int identity;
};

See a demo at http://ideone.com/SymduV

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • 2
    With your assumption I think it would be more efficient and safer to check the typeid equality in the base class operator and use static cast directly in the equals function. Using the dynamic_cast means that the if T has another derived class, call it X one could compare an object of type T and X through the base class and found them equal even if only the common T part is actually equivalent. Maybe in some cases it is what you want but in most others it would be an error. – Triskeldeian May 01 '17 at 10:37
  • @Triskeldeian you make a good point, but at some level you expect derived classes to make good on their is-a promise. I see the technique I show above to be more about an interface-level implementation. – Mark Ransom May 01 '17 at 15:06
  • 1
    What really matters, IMHO, is that the developer is conscious of the risks and assumptions on either of the techniques. Ideally I perfectly agree with you but on the practical point of you, considering that I work mostly with relatively inexperienced programmers, that choice can be more dangerous as it can introduce a very subtle error, difficult to spot, which creeps in unexpectedly. – Triskeldeian May 02 '17 at 19:14
  • This solution does not work if copy/move constructors are marked as deleted. See this example: https://godbolt.org/z/o93KxqE7c – cpchung Mar 28 '22 at 23:24
  • @cpchung then don't delete them, just mark them as private or protected. Or find another mechanism to make a copy. – Mark Ransom Mar 29 '22 at 00:59
  • The motivation there is to forbid copy. – cpchung Mar 29 '22 at 02:07
  • @cpchung the question was all about making copies, but in a controlled manner. Private/protected gives you that control. If you want to forbid copying completely, you shouldn't be paying any attention to this question. – Mark Ransom Mar 29 '22 at 04:16
0
  1. I think this looks weird:

    void foo(const MyClass& lhs, const MyClass& rhs) {
      if (lhs == rhs) {
        MyClass tmp = rhs;
        // is tmp == rhs true?
      }
    }
    
  2. If implementing operator== seems like a legit question, consider type erasure (consider type erasure anyways, it's a lovely technique). Here is Sean Parent describing it. Then you still have to do some multiple-dispatching. It's an unpleasant problem. Here is a talk about it.

  3. Consider using variants instead of hierarchy. They can do this type of things easyly.

Denis Yaroshevskiy
  • 1,218
  • 11
  • 24