I have a number of structs that are derived from the same base for the purpose of code reuse, but I do not want any form of polymorphism.
struct B {
int field;
void doStuff() {}
bool operator==(const B& b) {
return field == b.field;
}
};
struct D1 : public B {
D1(int field) : B{field} {}
};
struct D2 : public B {
D2(int field) : B{field} {}
};
Structs D1
and D2
(and more similar structs) derive from B
to share common fields and methods, so that I would not need to duplicate those fields and methods in each of the derived classes.
Struct B
is never instantiated; I only use instances of D1
and D2
. Furthermore, D1
and D2
are not supposed to interact with each other at all. Essentially, I do not want any polymorphic behaviour: D1
and D2
, for all purposes, should act as unrelated structs.
I would like any D1
to be compared with other D1
s for equality, and any D2
to be compared with other D2
s for equality. Since D1
and D2
don't contain any fields, it would seem appropriate to define an equality operator in struct B
.
However, (as expected) I get the following interaction between D1
and D2
:
int main() {
D1 d1a(1);
D1 d1b(1);
D2 d2(1);
assert(d1a == d1b); // good
assert(d1a == d2); // oh no, it compiles!
}
I don't want to be able to compare D1
with D2
objects, because for all purposes, they should act as if they are unrelated.
How can I make the last assertion be a compile error without duplicating code? Defining the equality operator separately for D1
and D2
(and all the other similar structs) would mean code duplication, so I wish to avoid that if possible.
"Structs D1 and D2 (and more similar structs) derive from B to share common fields and methods"
Then make B
a private
base class. Obviously, D1
and D2
shouldn't share their equality operator, since the two operators take different arguments. You can of course share part of the implementation as bool B::equal(B const&) const
, as this won't be accessible to outside users.
You can use CRTP to define operator ==
only on the reference to the base class of the final type:
template<typename T>
struct B {
int field;
void doStuff() {}
bool operator==(const B<T>& b) {
return field == b.field;
}
};
struct D1 : public B<D1> {
D1(int field) : B{field} {}
};
struct D2 : public B<D2> {
D2(int field) : B{field} {}
};
This causes the first assert
to compile and the second one to be rejected.
Instead of defining your equality operator as part of the base class, you usually need two functions in the derived classes:
struct B {
int field;
void doStuff() {}
};
struct D1 : public B {
D1(int field) : B{field} {}
bool operator==(const D1& d) {
return field == d.field;
}
};
struct D2 : public B {
D2(int field) : B{field} {}
bool operator==(const D2& d) {
return field == d.field;
}
};
Or, as is generally preferred, you could make them free functions:
bool operator==(const D1 &lhs, const D1 &rhs)
{
return lhs.field == rhs.field;
}
bool operator==(const D2 &lhs, const D2 &rhs)
{
return lhs.field == rhs.field;
}
Note: If field
was not a public member, you would need to declare the free function version as a friend
.
Okay, so maybe you have D3
through D99
, as well, some of which are indirect descendant of B
. You'll need to use templates:
template <class T>
bool operator==(const T &lhs, const T &rhs)
{
return lhs.field == rhs.field;
}
Great! But this grabs everything, which is bad for unrelated types that are not supposed to be comparable. So we need constraints.
Here's a trivial implementation without any code duplication (i.e. works for an arbitrary number of derived types):
template <class T, class = std::enable_if<std::is_base_of<B,T>()
&& !std::is_same<B, std::remove_cv_t<std::remove_reference_t<T>>>()>>
bool operator==(const T &lhs, const T &rhs)
{
return lhs.field == rhs.field;
}
The enable_if
checks first that T
inherits from B
then ensures it's not B
. You stated in your question that B
is basically an abstract type and is never directly implemented, but it's a compile-time test, so why not be paranoid?
As you later noted in comments, not all D#
are derived directly from B
. This will still work.
Given the following:
D1 d1(1);
D2 d2(2);
d1 == d2;
The compiler has to find a comparison operator, whether a free function or member of D1
(not D2
). Thankfully, you've defined one in class B
. The third line above can equivalently be stated:
d1.operator==(d2)
operator==
, however, is part of B
, so we're basically calling B::operator==(const B &)
. Why does this work when D2
is not B
? A language lawyer would clarify if it's technically argument dependent lookup (ADL) or overload resolution, but the effect is that D2
is silently cast to B
as part of the function call, making this equivalent to the above:
d1.operator==(static_cast<B>(d2));
This happens because no better comparison function can be found. Since there's no alternative, the compiler selects B::operator==(const B &)
and makes the cast.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With