Некоторое время назад я определил свой первый оператор трехстороннего сравнения. Он сравнил один тип и заменил несколько обычных операторов. Отличная функция. Затем я попытался реализовать аналогичный оператор для сравнения двух вариантов делегированием:
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 возвращают разные типы.
Каков правильный способ решить эту проблему?
@ Берги Ты прав. Вот почему в моем реальном коде я проверяю равенство типов.
Точно так же вы разрешаете любую другую функцию, которая возвращает auto, в которой разные операторы return выводят по-разному. Вы либо:
return
имеют один и тот же тип, илиВ этом случае 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 Да. std::strong_order на самом деле обеспечивает полный порядок с плавающей запятой, включая все NaN (это ISO/IEC/IEEE 60559 totalOrder). Это, конечно, больше работы, чем ветвление, но это также совсем другое поведение.
Я это понимаю, но я спрашивал конкретно о тех ветках, где он такой же. Кажется, что эти ветки должны быть одинаковыми, и производительность должна быть такой же, если значения будут использовать эти ветки, не так ли?
@KRyan Я не понимаю, о чем ты спрашиваешь.
Интересно, std::strong_order(double, double ); не компилируется на g++ 11 w. станд=С++20. Нет ли реализации для strong_ordering поплавков в g++?
Типы 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: как так? В любом случае вам нужно перечислить свои типы и способы их извлечения, и я уже указал вам, как сделать несколько случаев легко читаемыми. Если вы так склонны, вы можете поместить логику в вариативную функцию, где вы эффективно указываете тип для функции доступа-lmbda и имеете там логику, но она просто разбивается на одно и то же.
По-королевски склонен, да ;-) Хм, мне тоже пришло в голову использовать функцию с переменным числом аргументов. Я думаю, это может быть даже лучшим решением.
@Silicomancer на самом деле зависит от того, как вы его вращаете. Просто для этого варианта использования было бы больше ввода и запутывания. Если у вас есть подходящая бинарная функция apply(), которая принимает операцию (в данном случае [](auto const& l, auto const& r)( return l <=> r; }) в качестве аргумента, это может быть неплохо: return apply(op, l, r); Однако запись apply() немного раздражает, хотя и механически.
Посмотрите на мой ответ, что вы думаете?
@Silicomancer Интересно - я не использую Qt, поэтому не знаю интерфейса. На самом деле это кажется закрытым набором типов, выбор которых доступен с помощью value<T>(): в этом случае действительно возможна довольно общая реализация: я предположил, что вам нужно будет отображать вещи через toInt(), toDouble() и т. д. (что должно заставили меня понять, что она должна быть закрыта). Я думаю, что вы могли бы избежать необходимости в common_type, по-прежнему используя тернер в рекурсии и используя std::strong_order для завершения.
На самом деле это не закрытый набор. Его можно расширить во время компиляции с помощью макроса старого стиля. Функции toXxx() доступны только для большинства (всех?) встроенных типов. value() можно использовать в любом случае, но это необходимо для пользовательских типов.
Я поэкспериментировал с кодом шаблона, чтобы реализовать идею Дитмара Кюля об использовании 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 можно легко добавить дополнительные типы.
Разве <=> не нужно вести себя симметрично? Включая только l.type(), вы нарушаете это свойство.