Оператор трехстороннего сравнения с противоречивым выводом порядка

Некоторое время назад я определил свой первый оператор трехстороннего сравнения. Он сравнил один тип и заменил несколько обычных операторов. Отличная функция. Затем я попытался реализовать аналогичный оператор для сравнения двух вариантов делегированием:

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

Это не компилируется, я получаю ошибку

несогласованный вывод для типа автоматического возврата: «std::strong_ordering», а затем «std::partial_ordering».

Очевидно, что операторы космических кораблей int и double возвращают разные типы.

Каков правильный способ решить эту проблему?

Разве <=> не нужно вести себя симметрично? Включая только l.type(), вы нарушаете это свойство.

Bergi 19.12.2020 14:33

@ Берги Ты прав. Вот почему в моем реальном коде я проверяю равенство типов.

Silicomancer 19.12.2020 16:40
Почему в Python есть оператор &quot;pass&quot;?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
18
2
1 386
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Точно так же вы разрешаете любую другую функцию, которая возвращает auto, в которой разные операторы return выводят по-разному. Вы либо:

  1. Убедитесь, что все return имеют один и тот же тип, или
  2. Явно выберите тип возвращаемого значения.

В этом случае ints сравнивается как strong_ordering, а doubles сравнивается как 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 19.12.2020 15:30

@KRyan Да. std::strong_order на самом деле обеспечивает полный порядок с плавающей запятой, включая все NaN (это ISO/IEC/IEEE 60559 totalOrder). Это, конечно, больше работы, чем ветвление, но это также совсем другое поведение.

Barry 19.12.2020 17:57

Я это понимаю, но я спрашивал конкретно о тех ветках, где он такой же. Кажется, что эти ветки должны быть одинаковыми, и производительность должна быть такой же, если значения будут использовать эти ветки, не так ли?

KRyan 19.12.2020 18:20

@KRyan Я не понимаю, о чем ты спрашиваешь.

Barry 19.12.2020 18:26

Интересно, std::strong_order(double, double ); не компилируется на g++ 11 w. станд=С++20. Нет ли реализации для strong_ordering поплавков в g++?

TeaAge Solutions 21.01.2023 11:42
Ответ принят как подходящий

Типы operator<=> для int и double различаются, но они должны иметь общий тип. Возможно, вы захотите использовать компилятор для автоматического поиска нужного типа. Вы могли бы использовать std::common_type, но это было бы довольно уродливо. Проще просто использовать то, что std::common_type 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 18.12.2020 21:23

@Silicomancer: как так? В любом случае вам нужно перечислить свои типы и способы их извлечения, и я уже указал вам, как сделать несколько случаев легко читаемыми. Если вы так склонны, вы можете поместить логику в вариативную функцию, где вы эффективно указываете тип для функции доступа-lmbda и имеете там логику, но она просто разбивается на одно и то же.

Dietmar Kühl 18.12.2020 21:27

По-королевски склонен, да ;-) Хм, мне тоже пришло в голову использовать функцию с переменным числом аргументов. Я думаю, это может быть даже лучшим решением.

Silicomancer 18.12.2020 21:50

@Silicomancer на самом деле зависит от того, как вы его вращаете. Просто для этого варианта использования было бы больше ввода и запутывания. Если у вас есть подходящая бинарная функция apply(), которая принимает операцию (в данном случае [](auto const& l, auto const& r)( return l <=> r; }) в качестве аргумента, это может быть неплохо: return apply(op, l, r); Однако запись apply() немного раздражает, хотя и механически.

Dietmar Kühl 18.12.2020 21:56

Посмотрите на мой ответ, что вы думаете?

Silicomancer 20.12.2020 00:27

@Silicomancer Интересно - я не использую Qt, поэтому не знаю интерфейса. На самом деле это кажется закрытым набором типов, выбор которых доступен с помощью value<T>(): в этом случае действительно возможна довольно общая реализация: я предположил, что вам нужно будет отображать вещи через toInt(), toDouble() и т. д. (что должно заставили меня понять, что она должна быть закрыта). Я думаю, что вы могли бы избежать необходимости в common_type, по-прежнему используя тернер в рекурсии и используя std::strong_order для завершения.

Dietmar Kühl 20.12.2020 00:40

На самом деле это не закрытый набор. Его можно расширить во время компиляции с помощью макроса старого стиля. Функции toXxx() доступны только для большинства (всех?) встроенных типов. value() можно использовать в любом случае, но это необходимо для пользовательских типов.

Silicomancer 20.12.2020 01:18

Я поэкспериментировал с кодом шаблона, чтобы реализовать идею Дитмара Кюля об использовании 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 можно легко добавить дополнительные типы.

Другие вопросы по теме