c++c++20spaceship-operatordefault-comparisons

c++20 default comparison operator and empty base class


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.
};

Solution

  • 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... */
      }
    };