三路比较运算符与不一致排序推断

18

不久前,我定义了我的第一个三路比较运算符。它比较单个类型并替换了多个传统运算符。这是一个很棒的特性。然后,我尝试通过委托实现一个类似的比较两个变体的运算符:

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

这段代码无法编译,我收到了以下错误:

auto 返回类型无法一致推断出 ‘std::strong_ordering’ 和 ‘std::partial_ordering’。

显然,intdouble 的 spaceship 运算符返回不同的类型。

有什么正确的解决方法吗?


1
<=> 不需要表现对称吗?仅通过打开 l.type(),您就违反了该属性。 - Bergi
@Bergi 你是对的。这就是为什么在我的真实代码中我会检查类型相等性的原因。 - Silicomancer
3个回答

22

解决返回 auto 的函数不同的 return 语句推导出不同类型的方式与解决其他任何返回 auto 的函数相同。您可以:

  1. 确保所有的 return 语句都有相同的类型,或者
  2. 明确选择一个返回类型。

在这种情况下,int 比较为 strong_ordering,而 double 比较为 partial_ordering,且 strong_ordering 隐式可转换为 partial_ordering,您可以执行以下任一操作:

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

或者明确地将整数比较转换为:

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

这将给你一个返回partial_ordering的函数。


如果你想返回strong_ordering,那么你需要将double比较提升到更高的类别。你可以用两种方法实现:

你可以使用std::strong_order,这是一种更昂贵的操作,但提供了所有浮点数值的完全排序。你可以这样写:

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

或者你可以像考虑 NaN 无效一样,采取某些方法将其排除:

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

这种提升的方式可能更加繁琐,但我不确定是否有更直接的方法。


std::strong_order 在那些可以将部分排序简单转换为相应的强排序的情况下,是否会对性能造成影响?也就是说,这三种情况是否与 std::strong_order 对于这些情况所做的事情相同?(如果您认为这些值不合法并希望快速失败,则仍然可能有理由在无序结果上抛出异常,因此这是一个好建议,但我正在尝试更好地了解选项。) - KRyan
@KRyan 是的。std::strong_order 实际上为浮点数提供了一个全序,包括所有的 NaN(这是 ISO/IEC/IEEE 60559 的 totalOrder)。这肯定比分支更费力 -- 但它也是非常不同的行为。 - Barry
我理解您的意思,但我具体询问的是那些分支相同的情况。看起来那些分支应该是一样的,如果值使用那些分支,性能应该也是一样的,不是吗? - KRyan
@KRyan 我不明白你在问什么。 - Barry
在g++ 11 w. std=c++20上,std::strong_order(double, double)无法编译。g++中没有实现floats的strong_ordering吗? - TeaAge Solutions

5
operator<=>对于intdouble的类型不同,但它们应该有一个共同的类型。你可能希望利用编译器自动找到正确的类型。你可以使用std::common_type来实现,但那样会很丑陋。更简单的方法是利用std::common_type类型在库实现中的作用,并使用三元运算符:

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

听起来对于两种类型是个绝妙的解决方案。但是,我的实际函数将有数十种类型。那不会很混乱吗? - Silicomancer
@Silicomancer:怎么了?你需要列出你的类型及其如何提取,我已经告诉你了如何轻松地读取多个案例。如果你愿意,你可以将逻辑放入可变参数函数中,其中你有效地拼写出类型访问器-lmbda函数并在那里实现逻辑,但它会最终导致相同的事情。 - Dietmar Kühl
@Silicomancer 其实这取决于你如何看待它。仅针对此用例而言,这将导致更多的打字和混淆。如果您有一个适当的二进制 apply() 函数,该函数接受操作(在本例中为 [](auto const& l, auto const& r)( return l <=> r; })作为参数,那么它可能会很好:return apply(op, l, r); 然而,编写 apply() 有点烦人,尽管是机械化的。 - Dietmar Kühl
看看我的回答,你觉得怎么样? - Silicomancer
@Silicomancer 有趣 - 我不使用Qt,所以我不知道界面。实际上,它似乎是一组封闭类型,其选择可通过value<T>()访问:在这种情况下,确实可以实现相当通用的实现:我曾经认为您需要通过toInt()toDouble()等映射事物(这应该让我意识到它必须是封闭的)。我认为您可以通过仍然在递归中使用三元运算符并使用std::strong_order来终止来避免使用common_type - Dietmar Kühl
显示剩余2条评论

0

我尝试了一些模板代码来实现Dietmar Kühls使用std::common_type的想法。这是一个示例代码的结果:

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

可以轻松地将其他类型添加到variantSpaceship调用中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接