Prevent equality comparison of sibling structs
Asked Answered
M

4

7

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.

Miramirabeau answered 16/7, 2018 at 12:54 Comment(5)
There is a std::is_base_of functionLapwing
@Lapwing I am aware of that function but I fail to see how it would help to solve my problem.Miramirabeau
I suggest you do some research about the Curiously recurring template pattern.Stringpiece
Is the C++17 tag needed?Notorious
@JonHarper It's to say that I'm open to solutions that use stuff from the 2017 standard.Miramirabeau
H
4

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.

Hackery answered 16/7, 2018 at 13:0 Comment(13)
It turns out that I could also make other simplifications with my code using CRTP. I'm going with this solution. A possible danger with CRTP is that I can't put a static_assert in class B to make sure T indeed derives from B<T>, which means that carelessness here might result in some bugs at run-time.Miramirabeau
Any reason for the downvotes? This seems to be the best answer and I am keen to accept this.Miramirabeau
@Miramirabeau If I understand your concern correctly, you would like to prevent someone from passing B<X>::operator== a const X& instead of a const B<X>&, which would compile as long as X happened to have a field named field. If so, you can change field == b.field to field == static_cast<const B<T>&>(b).field. It's a bit more verbose, but static_cast should ensure T derives from B<T>. Here is the code amended with a == that reproduces the issue.Hackery
@Miramirabeau The downvoters chose to remain silent, but I guess the downvotes come from the general preference for a function operator as seen in other answers. Also, is the static_cast variant useful? If so, I'll include it in the answer.Hackery
Yes, it is useful :) A class-wide solution would be preferred (because inheritance is really a property of the class instead of operator==), but in the lack of other alternatives this would have to suffice.Miramirabeau
@Miramirabeau I've amended the answer with a simpler solution, constraining the type of the argument of operator == to const B<T>&, which automatically rejects T&. I'm not sure that a class-wide solution would apply - the problem was in the argument of operator ==, which is a property of the operator.Hackery
I think I might have misunderstood you. When I said "I can't put a static_assert in class B to make sure T indeed derives from B<T>", I meant that I want to protect against accidentally writing class D3 : public B<D2> instead of class D3 : public B<D3>, meaning that ideally the former should be a compilation error.Miramirabeau
@Miramirabeau Now I see what you mean. The solution in this answer covers that, I think.Hackery
Thanks for your help, but I've already read that and it would only work for a inheritance that is only one level deep. Unfortunately, in my use case I have about ten leaf classes (i.e. those that will be actually instantiated and used by other parts of the code) and the inheritance goes about three levels deep. I've looked at all the other answers as well and all the class-wide solutions are hacks in one way or another. Of all the imperfect choices over there, I think the macro answer is the cleanest choice (but it is still really yucky).Miramirabeau
@Miramirabeau FYI, my answer solves this leaf class issue (and still only requires one template function.Notorious
@JonHarper However, your answer doesn't use CRTP, which the OP discovered fits his needs for other things. The "leaf class issue" comes from attempting to prevent accidentally writing class D3: public B<D2> (regardless of the use of operator ==).Hackery
Darn it, I missed that comment. Thanks.Notorious
@JonHarper Yeah, both your answer and lubgr's answer solves this issue satisfactorily as well. But I've come to realize that CRTP is more extensible - in particular, I can have other member functions written in a similar way to the member operator==.Miramirabeau
K
7

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

Katelyn answered 16/7, 2018 at 13:15 Comment(2)
That means doStuff() can't be called by other classes, though. So I'll have to write forwarding functions in all my derived classes.Miramirabeau
@Bernard: That's what using declarations for members are for. You can publicly using a member of a privately inherited class.Greenhaw
H
4

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.

Hackery answered 16/7, 2018 at 13:0 Comment(13)
It turns out that I could also make other simplifications with my code using CRTP. I'm going with this solution. A possible danger with CRTP is that I can't put a static_assert in class B to make sure T indeed derives from B<T>, which means that carelessness here might result in some bugs at run-time.Miramirabeau
Any reason for the downvotes? This seems to be the best answer and I am keen to accept this.Miramirabeau
@Miramirabeau If I understand your concern correctly, you would like to prevent someone from passing B<X>::operator== a const X& instead of a const B<X>&, which would compile as long as X happened to have a field named field. If so, you can change field == b.field to field == static_cast<const B<T>&>(b).field. It's a bit more verbose, but static_cast should ensure T derives from B<T>. Here is the code amended with a == that reproduces the issue.Hackery
@Miramirabeau The downvoters chose to remain silent, but I guess the downvotes come from the general preference for a function operator as seen in other answers. Also, is the static_cast variant useful? If so, I'll include it in the answer.Hackery
Yes, it is useful :) A class-wide solution would be preferred (because inheritance is really a property of the class instead of operator==), but in the lack of other alternatives this would have to suffice.Miramirabeau
@Miramirabeau I've amended the answer with a simpler solution, constraining the type of the argument of operator == to const B<T>&, which automatically rejects T&. I'm not sure that a class-wide solution would apply - the problem was in the argument of operator ==, which is a property of the operator.Hackery
I think I might have misunderstood you. When I said "I can't put a static_assert in class B to make sure T indeed derives from B<T>", I meant that I want to protect against accidentally writing class D3 : public B<D2> instead of class D3 : public B<D3>, meaning that ideally the former should be a compilation error.Miramirabeau
@Miramirabeau Now I see what you mean. The solution in this answer covers that, I think.Hackery
Thanks for your help, but I've already read that and it would only work for a inheritance that is only one level deep. Unfortunately, in my use case I have about ten leaf classes (i.e. those that will be actually instantiated and used by other parts of the code) and the inheritance goes about three levels deep. I've looked at all the other answers as well and all the class-wide solutions are hacks in one way or another. Of all the imperfect choices over there, I think the macro answer is the cleanest choice (but it is still really yucky).Miramirabeau
@Miramirabeau FYI, my answer solves this leaf class issue (and still only requires one template function.Notorious
@JonHarper However, your answer doesn't use CRTP, which the OP discovered fits his needs for other things. The "leaf class issue" comes from attempting to prevent accidentally writing class D3: public B<D2> (regardless of the use of operator ==).Hackery
Darn it, I missed that comment. Thanks.Notorious
@JonHarper Yeah, both your answer and lubgr's answer solves this issue satisfactorily as well. But I've come to realize that CRTP is more extensible - in particular, I can have other member functions written in a similar way to the member operator==.Miramirabeau
N
3

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.

Notorious answered 16/7, 2018 at 13:51 Comment(2)
While this works, it doesn't answer my question, which asks for a solution that avoids code duplication of operator==.Miramirabeau
@Miramirabeau Whoops. Helps to answer the questions correctly. Updated with my apologies.Notorious
B
2

You can remove the equality operator from your original definition of the struct and replace it with a function template accepting two identical parameter types:

template <class T> bool operator == (const T& lhs, const T& rhs)
{
    return lhs.field == rhs.field;
}

Note that this function is somewhat "greedy", it might be best to put it in a namespace (together with the structs, to enable ADL) or to further constrain the types like this:

#include <type_traits>

template <class T, std::enable_if_t<std::is_base_of_v<B, T>, int> = 0>
bool operator == (const T& lhs, const T& rhs)
{
    return lhs.field == rhs.field;
}

(note that std::is_base_of_v requires C++17, but the verbose counterpart exists since C++11).

As a last tweak, in order to prevent such an explicit instantiation:

operator == <B>(d1a,  d2); // ultra-weird usage scenario, but compiles!

or (as @Aconcagua pointed out in the comments) type deduction with base-class references to the derived structs,

B& b1 = d1a;
B& b2 = d2;

assert(b1 == b2); // Compiles, but see below.

you also might want to add

template <> bool operator == <B>(const B&, const B&) = delete;
Blacktop answered 16/7, 2018 at 13:9 Comment(6)
More important: if not deleting, we could do: D1 d1; D2 d2; B& b1 = d1; B& b2 = d2; bool eq = b1 == b2;.Erbium
@Erbium Right, thanks for the hint! I'll incorporate that.Blacktop
Your template function lacks constraints. This means that struct Unrelated { char field; }; could participate in overload resolution. That's probably being too greedy.Notorious
Add std::enable_if?Notorious
With the right SFINAE to prevent this function from being too greedy, this would also work as good as CRTP (assuming I only need operators, which can be declared outside the class definition).Miramirabeau
@Miramirabeau Thanks for your suggestions, I added such a constraint to the answer.Blacktop

© 2022 - 2024 — McMap. All rights reserved.