Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent equality comparison of sibling structs

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 D1s for equality, and any D2 to be compared with other D2s 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.

like image 285
Bernard Avatar asked Jul 16 '18 12:07

Bernard


3 Answers

"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.

like image 184
MSalters Avatar answered Sep 24 '22 13:09

MSalters


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.

like image 22
user4815162342 Avatar answered Sep 24 '22 13:09

user4815162342


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.

Handling a large number of arbitrary types

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.

TL;DR; Solution

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.

Why you are having this issue

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.

like image 45
jonspaceharper Avatar answered Sep 22 '22 13:09

jonspaceharper