Search code examples
c++ooppolymorphismoperator-overloadingoverloading

How to overload polymorphic == and != operator in c++


class Media {
public:
    bool operator==(const Media& other) const {}
    bool operator!=(const Media& other) const {}
};

class Book : public Media {
public:
    bool operator==(const Book& other) const {} // commenting out this line solves this issue.
    bool operator!=(const Book& other) const {}
};

class Game : public Media {
public:
    bool operator==(const Game& other) const {}
    bool operator!=(const Game& other) const {}
};

int main() {
    Book book;
    Game game;

    bool res = book == game;  // doesn't compile.
}

I have these 3 classes and they must have their own == and != operators defined. But then I also have to compare between two siblings using those operators.

I could've written a (pure) virtual function, say, virtual bool equals(const Media& other) const in the base class that subclasses override. And then call that function in the bodies of == and != opertor definition in base class Media. But that feature is gone when I add another bool operator==(const Book& other) const {} in the Book class (the same goes for the Game class too).

Now I want to compare between siblings using those operators and still have all 6 definition in those 3 classes. How do I make it work?


Solution

  • You mentioned in the comments that this form of comparison is an imposed restriction (to compare among siblings of a child type). If its an imposed restriction that you need to somehow perform this with inheritance, then one option is to fulfill the base signature and use dynamic_cast. Note that this is not a clean approach, but it might be the expected solution for this problem if this is some form of assignment.

    dynamic_cast uses Runtime Type Information (RTTI) to determine whether an instance to a base class is actually an instance of the derived class. When you use it with a pointer argument, it returns nullptr on failure -- which is easily testable:

    auto p = dynamic_cast<const Book*>(&other);
    if (p == nullptr) { // other is not a book
      return false;
    }
    // compare books
    

    You can use this along with a virtual function to satisfy the hierarchy. However, to avoid possible ambiguities with 's generated symmetric operator==/operator!= functions, it's usually better to do this through a named virtual function rather than the operator== itself in order to prevent ambiguity:

    class Media {
    public:
      virtual ~Media() = default;
    
      bool operator==(const Media& other) const { return do_equals(other); }
    
    private:
      virtual bool do_equals(const Media& other) const = 0;
    };
    
    class Book : public Media {
      ...
    private:
      bool do_equals(const Media& other) const override {
        auto* p = dynamic_cast<const Book*>(&other);
        if (p == nullptr) { return false; }
    
        return (... some comparison logic ...);
      }
      ...
    };
    
    ... Same with Game ...
    
    

    Since we never define operator==(const Book&) or operator==(const Game&), we won't see this shadow the base-class' operator==; instead it always dispatches through the base's operator==(const Media&) -- which is non-virtual and prevents ambiguity.

    This would allow a Book and a Game to be comparable, but to return false -- whereas two Book or two Game objects may be compared with the appropriate logic.

    Live Example


    That said...

    This approach is not a good design, as far as software architecture goes. It requires the derived class to query what the type is -- and usually by the time you need to do this, that's an indication that the logic is funky. And when it comes to equality operators, it also leads to complications with symmetry -- where a different derived class may choose to compare things weirdly with different types (imagine a Media that may compare true with other different media; at which point, the order matters for the function call).

    A better approach in general is to define each of the respective equality operators between any types that logically require equality comparison. If you are in C++20 this is simple with symmetric equality generation; but pre-C++20 is a bit of a pain.

    If a Book is meant to be comparable to a Game, then define operator==(const Game&) or operator==(const Book&, const Game&). Yes, this may mean you have a large number of operator==s to define for each of them; but its much more coherent, and can get better symmetry (especially with C++20's symmetric equality):

    bool operator==(const Game&, const Book&);
    bool operator==(const Book&, const Game&); // Generated in C++20
    bool operator==(const Game&, const Game&);
    bool operator==(const Book&, const Book&);
    

    In an organization like this, Media may not even be logical as a 'Base class'. It may be more reasonable to consider some form of static polymorphism instead, such as using std::variant -- which is touched on in @Jarod42's answer. This would allow the types to be homogeneously stored and compared, but without requiring casting from the base to the derived type:

    // no inheritance:
    class Book { ... };
    class Game { ... };
    
    struct EqualityVisitor {
      // Compare media of the same type
      template <typename T>
      bool operator()(const T& lhs, const T& rhs) const { return lhs == rhs; }
    
      // Don't compare different media
      template <typename T, typename U>
      bool operator()(const T&, const U&) const { return false; }
    };
    
    class Media
    {
    public:
      ...
    
      bool operator==(const Media& other) const {
        return std::visit(EqualityVisitor{}, m_media, other.m_media);
      }
    private:
      std::variant<Book, Game> m_media;
    };
    

    Live Example

    This would be my recommended approach, provided the forms of media are meant to be fixed and not extended.