Search code examples
c++polymorphismcomparisonc++20comparison-operators

Comparing polymorphic types in c++20


I have code that is somewhere between c++17 and c++20. Specifically, we have c++20 enabled on GCC-9 and clang-9, where it is only partially implemented.

In code we have quite big hierarchy of polymorphic types like this:

struct Identifier {
    virtual bool operator==(const Identifier&other) const = 0;
};

struct UserIdentifier : public Identifier {
    int userId =0;
    bool operator==(const Identifier&other) const override {
        const UserIdentifier *otherUser = dynamic_cast<const UserIdentifier*>(&other);
        return otherUser && otherUser->userId == userId;
    }
};

struct MachineIdentifier : public Identifier {
    int machineId =0;
    bool operator==(const Identifier&other) const override {
        const MachineIdentifier *otherMachine = dynamic_cast<const MachineIdentifier*>(&other);
        return otherMachine && otherMachine->machineId == machineId;
    }
};

int main() {
    UserIdentifier user;
    MachineIdentifier machine;
    return user==machine? 1: 0;
}

https://godbolt.org/z/er4fsK

We are now migrating to GCC-10 and clang-10, but because of reasons we still need to work on versions 9 (well, at least clang-9 as this is what android NDK currently has).

The above code stops compiling because new rules about comparison operators are implemented. Reversible operator== causes ambiguities. I can't use a spaceship operator because it is not implemented in versions 9. But I omitted this from the example - I assume that whatever works with == will work with other operators.

So: What is the recommended approach to implementing comparison operators in c++20 with polymorphic types?


Solution

  • As an intermediate solution you could re-factor your polymorphic equality operator== to a non-virtual operator== defined in the base class, which polymorphically dispatches to a non-operator virtual member function:

    struct Identifier {    
        bool operator==(const Identifier& other) const {
            return isEqual(other);
        }
    private:
        virtual bool isEqual(const Identifier& other) const = 0;
    };
    
    // Note: do not derive this class further (less dyncasts may logically fail).
    struct UserIdentifier final : public Identifier {
        int userId = 0;
    private:
        virtual bool isEqual(const Identifier& other) const override {
            const UserIdentifier *otherUser = dynamic_cast<const UserIdentifier*>(&other);
            return otherUser && otherUser->userId == userId;
        }
    };
    
    // Note: do not derive this class further (less dyncasts may logically fail).
    struct MachineIdentifier final : public Identifier {
        int machineId = 0;
    private:
        virtual bool isEqual(const Identifier& other) const override {
            const MachineIdentifier *otherMachine = dynamic_cast<const MachineIdentifier*>(&other);
            return otherMachine && otherMachine->machineId == machineId;
        }
    };
    

    There will now no longer be an ambiguity as dispatch on the isEqual virtual member function will always be done on the left hand side argument to operator==.

    const bool result = (user == machine);  // user.isEqual(machine);