4

c++20 default comparison operator is a very convenient feature. But I find it less useful if the class has an empty base class.

The default operator<=> performs lexicographical comparison by successively comparing the base (left-to-right depth-first) and then non-static member (in declaration order) subobjects of T to compute <=>, recursively expanding array members (in order of increasing subscript), and stopping early when a not-equal result is found

According to the standard, the SComparable won't have an operator<=> if base doesn't have an operator<=>. In my opinion it's pointless to define comparison operators for empty classes. So the default comparison operators won't work for classes with an empty base class.

struct base {};

struct SComparable: base {
  int m_n;
  auto operator<=>(SComparable const&) const& = default; // default deleted, clang gives a warning
};

struct SNotComparable: base {
  int m_n;
};

If we are desperate to use default comparison operators and therefore define comparison operators for the empty base class base. The other derived class SNotComparable wrongly becomes comparable because of its empty base class base.

struct base {
  auto operator<=>(base const&) const& = default;
};

struct SComparable: base {
  int m_n;
  auto operator<=>(SComparable const&) const& = default;
};

struct SNotComparable: base { // SNotComparable is wrongly comparable!
  int m_n;
};

So what is the recommended solution for using default comparison operators for classes with an empty base class?

Edit: Some answers recommend to add default comparison operator in the empty base class and explicitly delete comparison operator in non-comparable derived classes.

If we add default comparison operator to a very commonly used empty base class, suddenly all its non-comparable derived classes are all comparable (always return std::strong_ordering::equal). We have to find all these derived non-comparable classes and explicitly delete their comparison operators. If we missed some class and later want to make it comparable but forget to customize its comparison operator (we all make mistakes), we get a wrong result instead of a compile error from not having default comparison operator in the empty base as before. Then why do I use default comparison operator in the first place? I would like to save some efforts instead of introducing more.

struct base {
  auto operator<=>(base const&) const& = default;
};

struct SComparable: base {
  int m_n;
  auto operator<=>(SComparable const&) const& = default;
};

struct SNotComparable1: base {
  int m_n;
  auto operator<=>(SNotComparable1 const&) const& = delete;
};

struct SNotComparableN: base {
  int m_n;
  // oops, forget to delete the comparison operator!
  // if later we want to make this class comparable but forget to customize comparison operator, we get a wrong result instead of a non-comparable compile error.
};
wanghan02
  • 1,227
  • 7
  • 14
  • Would you rather make it impossible to have a base class with a default comparator and not make it possible to inherit from that base without adding a default comparator to the derived class? That would break a lot of assumptions people have about inheritance I think. – Ted Lyngmo Aug 25 '22 at 10:40
  • 1
    `auto operator<=>(SNotComparable const&) const& = delete;` – n. m. could be an AI Aug 25 '22 at 11:29

3 Answers3

2

In my opinion it's pointless to define comparison operators for empty classes.

Well, it's clearly not pointless. If what you want to do is default your type's comparisons, that necessarily implies comparing all of your type's subobjects, including the base class subobjects, which requires them to be comparable - even if they're empty.

What you need to do is provide them - just conditionally. The simplest way of doing so is probably to provide a different empty base class:

struct base { /* ... */ };

struct comparable_base : base {
    friend constexpr auto operator==(comparable_base, comparable_base)
        -> bool
    {
        return true;
    }

    friend constexpr auto operator<=>(comparable_base, comparable_base)
        -> std::strong_ordering
    {
        return std::strong_ordering::equal;
    }
};

And then inherit from comparable_base when you want to have comparisons, and base when you don't. That is:

struct SComparable: comparable_base {
  int m_n;
  auto operator<=>(SComparable const&) const& = default;
};

struct SNotComparable: base {
  int m_n;
};

I'm using hidden friend comparisons there just to be able to take the type by value - since it's empty. Could just as easily be a member function too.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Yes, I think comparable_base for comparable derived classes should be the right way to do. But I think we should make comparable a template class that derives from its template Base and provides comparison operators as in your comparable_base. Then we could use it simply as comparable and it's generic. – wanghan02 Aug 25 '22 at 17:15
  • @wanghan02 You'd sort of loose the whole point of having an empty base class then, wouldn't you? If it's empty **and** unique to each derived class, why have it at all? – Ted Lyngmo Aug 25 '22 at 18:22
  • @TedLyngmo It's not unique to each derived class. Just use it for non-comparable classes and comparable classes with non-default comparator. And wrap it with the comparable mix-in for comparable classes with default comparator. – wanghan02 Aug 25 '22 at 18:42
  • @wanghan02 Ok, what template parameter should the template Base you talk about in _"derives from its template Base"_ have? – Ted Lyngmo Aug 25 '22 at 18:47
  • @TedLyngmo As they said: "use it simply as comparable" – Barry Aug 25 '22 at 18:59
  • @Barry Ok, so it was a typo? It should just have been _"derives from its Base"_? If so, no problem. – Ted Lyngmo Aug 25 '22 at 19:01
  • @TedLyngmo I... don't understand. The suggestion is that we have `template struct comparable : Base { ... }` and that `SComparable` inherits from `comparable`. I see they just posted that as [a separate answer](https://stackoverflow.com/a/73492697/2069064). – Barry Aug 25 '22 at 21:13
  • My question was to @wanghan02 who said _"But I think we should make comparable a template class that derives from its template Base"_ - that's the only thing I questioned. Not your answer. Your answer is similar to what I suggested as a comment under my own answer (only you made it better by inheritance). – Ted Lyngmo Aug 25 '22 at 21:15
0

what is the recommended solution for using default comparison operators for classes with an empty base class?

The solution is to add the default comparator to the base class and then do what you do in SComparable if you want the added member(s) of SComparable to be included in the comparison - just as with a base class with members.

If you don't want them to be included in the comparison, don't add a default comparator, like you do in SNotComparable - and the base class comparator will be used - again, just like in a base class with members.

If you don't want the base class behavior in SNotComparable and you don't want SNotComparable to be comparable, then delete the comparator, just like you would if the base class had members:

auto operator<=>(SNotComparable const&) const& = delete;
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • 1
    To fix non-comparable derived class by explicitly deleting its comparison operator might bring more errors. There might be many such derived classes and that brings a lot of work. If we accidently missed some, we get a wrong result if later we want to make it comparable but forget to customize its comparison operator. – wanghan02 Aug 25 '22 at 13:42
  • @wanghan02 If you on the other hand _don't_ delete it, you'll get the base class comparator. That's how the language works. Perhaps the design is flawed and you should have one base for things comparable and one for things not comparable? Impossible to say - but that's an option at least. – Ted Lyngmo Aug 25 '22 at 13:44
0

I'd like to make a small modification based on @Barry's answer. We could have a generic mix-in class comparable<EmptyBase> that provides comparable operators for any empty base. If we want to use default comparison operators for a class derived from empty base class(es), we can simple derive such class from comparable<base> instead of base. It also works for chained empty bases comparable<base1<base2>>.

struct base { /* ... */ };

template<typename EmptyBase>
struct comparable: EmptyBase {
    static_assert(std::is_empty<EmptyBase>::value);
    template<typename T> requires std::same_as<comparable>
    friend constexpr auto operator==(T const&, T const&)
        -> bool
    {
        return true;
    }

    template<typename T> requires std::same_as<comparable>
    friend constexpr auto operator<=>(T const&, T const&)
        -> std::strong_ordering
    {
        return std::strong_ordering::equal;
    }
};

struct SComparableDefault: comparable<base> {
  int m_n;
  auto operator<=>(SComparableDefault const&) const& = default;
};

struct SNotComparable: base {
  int m_n;
};

struct SComparableNotDefault: base {
  int m_n;
  constexpr bool operator==(SComparableNotDefault const& rhs) const& {
    /* user defined... */
  }
  constexpr auto operator<=>(SComparableNotDefault const& rhs) const& {
    /* user defined... */
  }
};
wanghan02
  • 1,227
  • 7
  • 14
  • @TedLyngmo Here is the example. – wanghan02 Aug 25 '22 at 19:18
  • I didn't get a notfification about this. Apparently, tagging people that haven't participated in the comments section doesn't work. Yes, this looks like an extension of what I suggested in my comment and that Barry showed in a proper answer. – Ted Lyngmo Aug 25 '22 at 21:23