This does not look like a problem of polymorphism. Actually, I think that there is any polymorphism at all is a symptom of a data model error.
If you have values that identify machines, and values that identify users, and these identifiers are not interchangeable¹, they should not share a supertype. The property of "being an identifier" is a fact about how the type is used in the data model to identify values of another type. A MachineIdentifier
is an identifier because it identifies a machine; a UserIdentifier
is an identifier because it identifies a user. But an Identifier
is in fact not an identifier, because it doesn't identify anything! It is a broken abstraction.
A more intuitive way to put this might be: the type is the only thing that makes an identifier meaningful. You cannot do anything with a bare Identifier
, unless you first downcast it to MachineIdentifier
or UserIdentifier
. So having an Identifier
class is most likely wrong, and comparing a MachineIdentifier
to a UserIdentifier
is a type error that should be detected by the compiler.
It seems to me the most likely reason Identifier
exists is because someone realized that there was common code between MachineIdentifier
and UserIdentifier
, and leapt to the conclusion that the common behavior should be extracted to an Identifier
base type, with the specific types inheriting from it. This is an understandable mistake for anyone who has learned in school that "inheritance enables code reuse" and has not yet realized that there are other kinds of code reuse.
What should they have written instead? How about a template? Template instantiations are not subtypes of the template or of each other. If you have types Machine
and User
that these identifiers represent, you might try writing a template Identifier
struct and specializing it, instead of subclassing it:
template <typename T>
struct Identifier {};
template <>
struct Identifier<User> {
int userId = 0;
bool operator==(const Identifier<User> &other) const {
return other.userId == userId;
}
};
template <>
struct Identifier<Machine> {
int machineId = 0;
bool operator==(const Identifier<Machine> &other) const {
return other.machineId == machineId;
}
};
This probably makes the most sense when you can move all the data and behavior into the template and thus not need to specialize. Otherwise, this is not necessarily the best option because you cannot specify that Identifier
instantiations must implement operator==
. I think there may be a way to achieve that, or something similar, using C++20 concepts, but instead, let's combine templates with inheritance to get some advantages of both:
template <typename Id>
struct Identifier {
virtual bool operator==(const Id &other) const = 0;
};
struct UserIdentifier : public Identifier<UserIdentifier> {
int userId = 0;
bool operator==(const UserIdentifier &other) const override {
return other.userId == userId;
}
};
struct MachineIdentifier : public Identifier<MachineIdentifier> {
int machineId = 0;
bool operator==(const MachineIdentifier &other) const override {
return other.machineId == machineId;
}
};
Now, comparing a MachineIdentifier
to a UserIdentifier
is a compile time error.
This technique is called the curiously recurring template pattern (also see crtp). It is somewhat baffling when you first come across it, but what it gives you is the ability to refer to the specific subclass type in the superclass (in this example, as Id
). It might also be a good option for you because, compared to most other options, it requires relatively few changes to code that already correctly uses MachineIdentifier
and UserIdentifier
.
¹ If the identifiers are interchangeable, then most of this answer (and most of the other answers) probably does not apply. But if that is the case, it should also be possible to compare them without downcasting.
dynamic_cast
usages? It's kinda the complete opposite of polymorphism IMO. Plus, it yields an additional runtime impact. Surely there's a way to avoid RTTI here. – Friscoa == b
may not be equivalent tob == a
. Example – Nafinal
to leaf classes along with a comment to that efcect might help avoid mistakes. – NaIdentifier
s of which you don't know the concrete type? Or isIdentifier
just a repository for common code? This seems like an decent application for the curiously recurring template pattern – SpringletSuperMachineIdentifier
inheriting fromMachineIdentifier
and adding a new "superId" field, and if so how does comparison work between an instance of each? – Krum