Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Three-way comparison operator with inconsistent ordering deduction

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?

like image 738
Silicomancer Avatar asked Dec 18 '20 19:12

Silicomancer


2 Answers

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.

like image 53
Barry Avatar answered Sep 19 '22 21:09

Barry


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;
}
like image 37
Dietmar Kühl Avatar answered Sep 19 '22 21:09

Dietmar Kühl