Three-way comparison operator with inconsistent ordering deduction

1.6k views Asked by At

Some time ago I defined my first three-way comparison operator. It compared a single type and replaced multiple conventional operators. Great feature. Then I tried to implement a similar operator for comparing two variants by delegation:

auto operator <=> (const QVariant& l, const QVariant& r)
{   
   switch (l.type())
   {
      case QMetaType::Int:
         return l.toInt() <=> r.toInt();
      case QMetaType::Double:
         return l.toDouble() <=> r.toDouble();
      default:
         throw;
   }
}

This doesn't compile, I get the error

inconsistent deduction for auto return type: ‘std::strong_ordering’ and then ‘std::partial_ordering’.

Obviously int and double spaceship operators return different types.

What is the correct way to solve this?

3

There are 3 answers

7
Dietmar Kühl On BEST ANSWER

The types of the operator<=> for int and double differ but they should have a common type. You probably want to leverage the compiler in automatically finding the proper type. You could use std::common_type to do but that would be quite ugly. It is easier to just leverage what std::common_type type does under the (when implemented in the library rather the compiler) and use the ternary operator:

auto operator <=> (const QVariant& l, const QVariant& r)
{   
    return l.type() == QMetaType:Int? l.toInt() <=> r.toInt()
         : l.type() == QMetaType::Double? l.toDouble() <=> r.toDouble()
         : throw;
}
5
Barry On

Same way you resolve any other function which returns auto in which different return statements deduce differently. You either:

  1. Ensure that all the returns have the same type, or
  2. Explicitly pick a return type.

In this case, ints compare as strong_ordering while doubles compare as partial_ordering, and strong_ordering is implicitly convertible to partial_ordering, you can do either:

std::partial_ordering operator <=>(const QVariant& l, const QVariant& r) {
    // rest as before
}

or explicitly cast the integer comparison:

      case QMetaType::Int:
         return std::partial_ordering(l.toInt() <=> r.toInt());

That gives you a function returning partial_ordering.


If you want to return strong_ordering instead, you have to lift the double comparison to a higher category. You can do that in two ways:

You can use std::strong_order, which is a more expensive operation, but provides a total ordering over all floating point values. You would then write:

      case QMetaType::Double:
         return std::strong_order(l.toDouble(), r.toDouble());

Or you can do something like consider NaNs ill-formed and throw them out somehow:

      case QMetaType::Double: {
         auto c = l.toDouble() <=> r.toDouble();
         if (c == std::partial_ordering::unordered) {
             throw something;
         } else if (c == std::partial_ordering::less) {
            return std::strong_ordering::less;
         } else if (c == std::partial_ordering::equivalent) {
            return std::strong_ordering::equal;
         } else {
            return std::strong_ordering::greater;
         }
      }

It's more tedious but I'm not sure if there's a more direct way to do this kind of lifting.

0
Silicomancer On

I played around with some template code to implement Dietmar Kühls idea of using std::common_type. This is the result example code:

template <typename CommonT, typename... ArgsT> requires (sizeof...(ArgsT) == 0)
inline CommonT variantSpaceshipHelper([[maybe_unused]] const QVariant& pLeft, [[maybe_unused]] const QVariant& pRight) noexcept
{
   std::terminate(); // Variant type does not match any of the given template types
}

template <typename CommonT, typename T, typename... ArgsT>
inline CommonT variantSpaceshipHelper(const QVariant& pLeft, const QVariant& pRight) noexcept
{
   if (pLeft.type() == static_cast<QVariant::Type>(qMetaTypeId<T>()))
   {
      return (pLeft.value<T>() <=> pRight.value<T>());
   }

   return variantSpaceshipHelper<CommonT, ArgsT...>(pLeft, pRight);
}

template <typename... ArgsT>
inline auto variantSpaceship(const QVariant& pLeft, const QVariant& pRight) noexcept
{
   using CommonT = std::common_type_t<decltype(std::declval<ArgsT>() <=> std::declval<ArgsT>())...>;
   return variantSpaceshipHelper<CommonT, ArgsT...>(pLeft, pRight);
}

inline auto operator <=>(const QVariant& pLeft, const QVariant& pRight) noexcept
{
   assert(pLeft.type() == pRight.type());
   return variantSpaceship<int, double>(pLeft, pRight);
}

Additional types can easily be added to the variantSpaceship call.