Can I make operator== virtual to implement equality?
Asked Answered
S

3

1

From this implementing operator== when using inheritance question, I see people say that operator== should not be made virtual (the accepted answer even says "operators cannot be virtual", which I found that is not true later):

c++ allows operators to be virtual. but i wouldn't make them virtual. polymorphism and operators actually don't fit together very well (and free operator functions obviously can't be virtual at all). ---- Johannes Schaub - litb

-1. Making Person::operator== virtual is not sufficient: Employee::operator== must be rewritten to have the same signature. Moreover, this would still lead to the issue pointed by Douglas, i.e. assymetry of the comparison operation, which is...weird. ---- Luc Touraille

I don't understand why I can't make operator== virtual. I wrote this code and seem to work well with polymorphism:

#include <iostream>
#include <typeinfo>
using namespace std;

struct Base {
    int b1 = 1;
    int b2 = 2;
    virtual bool operator==(const Base& rhs) const {
        cout << "Base" << endl;
        if (typeid(*this) != typeid(rhs)) {
            cout << "false" << endl;
            return false;
        }
        else {
            cout << "true" << endl;
            return (b1 == rhs.b1) && (b2 == rhs.b2);
        }
    }
};

struct Derived : public Base {
    int d1 = 3;
    int d2 = 4;
    virtual bool operator==(const Base& rhs) const override {
        cout << "Derived" << endl;
        if (typeid(*this) != typeid(rhs)) {
            cout << "false" << endl;
            return false;
        }
        else {
            cout << "true" << endl;
            const Derived& rrhs = static_cast<const Derived&>(rhs);
            return (b1 == rrhs.b1) && (b2 == rrhs.b2) && (d1 == rrhs.d1)
                && (d2 == rrhs.d2);
        }
    }
};

int main()
{
    Derived d;
    Base b1, b2;
    Base *bptr1, *bptr2, *bptr3;
    bptr1 = &b1;
    bptr2 = &d;
    bptr3 = &b2;
    
    if (*bptr2 == *bptr1) {
        cout << "1" << endl;
    }
    else {
        cout << "0" << endl;
    }

    if (*bptr3 == *bptr1) {
        cout << "1" << endl;
    }
    else {
        cout << "0" << endl;
    }

    if ((d == b1) != (b1 == d)) {
        cout << "xxx";
    }
    return 0;
}

Is there any problem with my code?

Smoothtongued answered 29/10, 2020 at 7:27 Comment(10)
Whenever you feel the need to do something like typeid(*this) != typeid(rhs) then you should take that as a sign you're doing something wrong. I would recommend that you create a non-member (but possibly friend) operator== function which then calls a virtual comparison function, as in friend bool operator==(Base const& lhs, Base const& rhs) { return lhs.compare_equal(rhs); }. Then override compare_equal in child objects as needed, with downcasts of the rhs.Kappenne
It is not question of "if it is possible", but how it impacts code readability and maintainability. So basically you can do it, but you should not.Lucie
O.T.: Are you aware that if (*bptr2 == *bptr1) { cout << "1" << endl; } else { cout << "0" << endl; } could be written more compact as cout << (*bptr2 == *bptr1) << endl; (with the exact same outcome?Skyjack
Operators can be virtual. Operators are just functions with funny names. The problem is not in the name, but rather in the semantics. Equality rarely makes sense for polymorphic objects, whether you call it bool operator= or bool equals or whatever. What is your application domain that requires equality comparison of this kind?Heartthrob
And now you need to implement that operator in every single derived class, even in ones that have the same conditions for equality as their base class, including the code duplication you get from not being able to use the "base" operator.Keithakeithley
@n.'pronouns'm. "What is your application domain that requires equality comparison of this kind?". I don't know. It's from C++ Primer 5th 19.2.3. Using RTTI, "As an example of when RTTI might be useful, consider a class hierarchy for which we’d like to implement the equality operator (§ 14.3.1, p. 561). Two objects are equal if they have the same type and same value for a given set of their data members. Each derived type may add its own data, which we will want to include when we test for equality......"Smoothtongued
Just ignore this part of the book. "Two objects are equal if they have the same type" this oversimplified approach is highly problematic. It violates LSP for one.Heartthrob
@Keithakeithley In which way can I avoid code duplication if I am going to implement this equality? I don't think code duplication can be avoided for this case.Smoothtongued
@Someprogrammerdude But you still can't avoid using typeid with your approach right? The book uses a non-member friend function as you describe to implement this. It looks like this wandbox.org/permlink/O8m6j3l2z3rvQ3xs . What's the differences between this and mine? Which is better?Smoothtongued
There's probably no need to make it virtual. If you are dealing with polymorphic objects, they are probably entity objects rather than value objects. So you only need to compare for identity (same memory address) rather than value or type. (I can't think of a scenario where type equality would be useful.)Topsoil
H
2

There is no way to define a generic comparison operator that would work in polymorphic class hierarchies. Here are a couple of examples.

Example one. We are making an arithmetic hierarchy. Operate on BigNumber and the underlying framework will choose the right representation.

class BigNumber { ... };
class BigInteger : public Number { ... };
class BigRational : public Number { ... };
class BigFloating : public Number { ... };

If we subscribe to the philosophy that two objects of different dynamic types are never equal, then BigInteger(0) is not equal to BigRational(0,1) or BigFloating(0.0). This breaks arithmetics as we know it.

Example two. We are making a geometry hierarchy.

class Shape { ... };
class Triangle : public Shape { ... };
class Rectangle : public Shape { ... };

So far so good. A Triangle is never equal to a Rectangle, right? Let's add colour.

class ColouredTriangle : public Triangle { ... };

Oops! Now our little CAD program has stopped working because we have replaced one of the Triangles with a ColouredTriangle, even though the geometry engine has no idea about colour and is only interested in geometric properties. Worse still, even if we replace all Triangles with ColouredTriangles, and if we want the geometry engine to work exactly as before, all the colours need to be the same.

Perhaps there are application domains where comparing type-IDs first is a sound approach to object comparison, but I have yet to see one.

This applies regardless of your programming language.

Heartthrob answered 29/10, 2020 at 9:57 Comment(0)
V
1

You get around the symmetry issue (a == b ought to give the same result as b == a) by using typeid and static_cast. Yes your code is well behaved. I included the lines

if (!(*bptr2 == *bptr1) && (*bptr1 == *bptr1)){
    std::cout << "Oops";
}
if (*bptr3 == *bptr1 && !(*bptr1 == *bptr3)){
    std::cout << "Oops";
}

by way of an extra test. So you're not really using polymorphism at all. You're pretty much undoing the effects that polymorphism has.

Virgule answered 29/10, 2020 at 7:41 Comment(1)
From the book it's written in this way wandbox.org/permlink/O8m6j3l2z3rvQ3xs, may I ask which one is better, compared to my verison?Smoothtongued
T
1

It is a question of the semantics you want in your program.

In the question you linked to, OP has Person and Employee classes. In their implementation you could have a person, Bob, be represented with both a Person object (pBob) and an Employee object (eBob), and then have the qurious situation that:

pBob == eBob -> true and eBob == pBob -> false

This is the asymmetric comparison that the comment you quoted warned about. This is something that, I believe, would catch most people by surprise - and thus be very undesirable code.

This problem would also get stranger if you add more classes: Contractor, Executive, Salaried, HourlyWorker. Now if you have two Employee&s you have to investigate what the underlying object types are, and possibly jump through hoops to compare them in the correct way: Bob was a contractor but has now been hired on a salary, how do you compare the contractorBob and salariedBob object with the Employee comparison operator to figure out that they are actually the same employee/person when all you have are virtual comparison operators?

On the other hand, it may be that you work in a domain where an asymmetric comparison operator is the obvious thing.

In the end it just comes down to what you want the semantics of your classes to be, and how to best implement the semantics that makes sense for you.

Thousandth answered 29/10, 2020 at 8:5 Comment(1)
Re-reading my own answer, I think that I managed to run off on a bit of a tangent on a general topic, and not so much answer OP's question. Unfortunately I don't know how to make it more of a direct answer to OP without basically restating what others have already said in comments and answers. I apologize for this, but will still leave my answer since I believe it brings up some valid general points.Thousandth

© 2022 - 2024 — McMap. All rights reserved.