Я наблюдаю неожиданное поведение при использовании std::variant::operator<
. В ситуации, когда тип имеет неявный оператор преобразования bool, а его оператор less не является функцией-членом (в C++20 с компилятором mscv 19.38).
#include <variant>
struct Foo {
int x;
int y;
#ifndef DROP_CAST_OP
constexpr operator bool() const { return x || y; }
#endif
#ifdef USE_SPACESHIP
constexpr auto operator<=>(const Foo&) const noexcept = default;
#else
friend constexpr bool operator<(const Foo& a, const Foo& b) noexcept
{
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
#endif
};
using TestVariant = std::variant<Foo, int>;
constexpr Foo fooA { 0, 1 };
constexpr Foo fooB { 1, 0 };
constexpr std::variant<Foo, int> varA = fooA;
constexpr std::variant<Foo, int> varB = fooB;
static_assert(fooA < fooB);
static_assert(varA < varB);
https://godbolt.org/z/1zfq5dq1r
Обратите внимание, что утверждение начинает проходить, когда выполняется одно из следующих условий:
Все компиляторы ведут себя одинаково.
Почему у тебя operator bool
вместо explicit operator bool
?
Так что если сделать это explicit operator bool
, это исправит ситуацию. Этот объект очень старый, поэтому он не был указан ранее.
Получил полную копию constexpr для всех основных компиляторов: godbolt.org/z/34fsqY5dn
@MarekR, спасибо, что так много вникал в это
Re: «Оператор приведения bool» — это оператор преобразования; приведение — это то, что вы пишете в исходном коде, чтобы сообщить компилятору выполнить преобразование.
Связано: stackoverflow.com/q/78346768/11910702
Хех, я точно знал, что это будет за код, когда прочитал заголовок. Я не могу найти подходящего объекта для обмана, поэтому постараюсь сделать это каноническим ответом.
В C++17 std::variant
(как и множество других шаблонов классов в стандартной библиотеке, std::pair
, std::tuple
и std::optional
среди них) определяют <
с точки зрения подчинения базовым типам <
. Единственной операцией, вызываемой над базовым типом, была T
.
В частности, то, что operator<
будет делать с двумя объектами типа variant<T, U>
(при условии, что <
определен как для T
, так и для U
), — это сначала сравнить индексы, а если они одинаковы, сравнить значения. Что-то вроде этого:
bool operator<(variant<T, U> const& lhs, variant<T, U> const& rhs) {
if (lhs.index() != rhs.index()) {
return lhs.index() < rhs.index();
}
// not this specifically, but this conceptually
return std::get<lhs.index()>(lhs) < std::get<rhs.index()>(rhs);
}
В C++20 появился <=>
, который, как правило, является гораздо лучшим способом работы с упорядочиванием и имеет множество удобств, упрощающих написание сравнений (равенство и порядок). Но также возникла проблема: до C++20 <=>
не было кода. Таким образом, мы не можем оптом просто изменить сравнение std::variant
на использование <=>
, потому что ни один существующий код не использует <=>
.
Вместо этого библиотека предпочтительно использует <=>
, но возвращается к <
, если <=>
недоступен. Это делается с помощью объекта только для спецификации с именем synth-three-way
, указанного в [expos.only.entity]:
constexpr auto synth-three-way = // exposition only
[]<class T, class U>(const T& t, const U& u)
requires requires {
{ t < u } -> boolean-testable;
{ u < t } -> boolean-testable;
}
{
if constexpr (three_way_comparable_with<T, U>) {
return t <=> u;
} else {
if (t < u) return weak_ordering::less;
if (u < t) return weak_ordering::greater;
return weak_ordering::equivalent;
}
};
Это довольно просто: если <=>
доступен, мы действительно хотим использовать <=>
. Но если <=>
недоступен, мы возвращаемся к тому, что нам приходилось делать в C++17, и используем <
.
И это поведение, которое вы хотите.
За исключением тех случаев, когда... это не так.
Давайте вернемся к вашему типу:
struct Foo {
int x;
int y;
#ifndef DROP_CAST_OP
constexpr operator bool() const { return x || y; }
#endif
#ifdef USE_SPACESHIP
constexpr auto operator<=>(const Foo&) const noexcept = default;
#else
friend constexpr bool operator<(const Foo& a, const Foo& b) noexcept
{
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
#endif
};
Мы можем пройти через различное поведение. Я предполагаю, что мы всегда предоставляем ровно один из <
или <=>
:
<
по сравнению с <
С++17
да
<
по сравнению с <
С++20
нет
<
по сравнению с <
С++20
да (неявно)
<
сравнивает результат преобразования с bool
(см. ниже)
С++20
да (явно)
<
по сравнению с <
С++20
нет
<=>
по сравнению с <=>
С++20
да (неявно)
<=>
по сравнению с <=>
С++20
да (явно)
<=>
по сравнению с <=>
Помните, что правило такое: если <=>
работает, используйте <=>
, в противном случае вернитесь к <
. Однако в языке нет механизма проверки того, как работает <=>
.
Когда вы предоставляете <=>
для сравнения Foo
, то <=>
существует, является жизнеспособным и является лучшим вариантом, поэтому неудивительно, что он используется.
Когда вы предоставляете <
для сравнения Foo
, это само по себе не обязательно означает, что <=>
нежизнеспособно. Когда вы обеспечиваете неявное преобразование в bool
, то f1 <=> f2
по-прежнему жизнеспособно — оно оценивается как (bool)f1 <=> (bool)f2
, поскольку доступны встроенные кандидаты. Это не относится только к bool
— любой встроенный тип (например, int
или char const*
) или другой тип, для которого ADL может найти кандидата, приведет к такому же поведению. Итак, согласно языку, сравнение двух Foo
с <=>
работает нормально — именно этот механизм мы предпочитаем в библиотеке. Просто в данном конкретном случае это дает удивительное поведение, поскольку вы, вероятно, предпочли явное <
неявному <=>
посредством неявного bool
преобразования.
Вот почему явная пометка оператора преобразования решает проблему: встроенный operator<=>(bool, bool)
больше не является жизнеспособным кандидатом, поэтому не существует реального способа вызвать <=>
для двух Foo
. Следовательно, библиотека возвращается к использованию <
.
Обратите внимание, что это даже не новая проблема. Если бы Foo
обеспечивал неявное преобразование в bool
, но не operator<
и не operator<=>
, то даже в C++17 сравнение variant
все равно работало бы: посредством неявного преобразования в bool
. Потому что вычисление t < u
было бы допустимым выражением для такого преобразования. Единственное нововведение здесь в том, что из-за приоритета <=>
даже предоставление <
не гарантирует, что библиотека будет использовать написанный вами оператор сравнения.
Эта проблема продолжает возникать, потому что люди пишут типы, которые имеют явные операторы сравнения (через <
), но также предоставляют неявную функцию преобразования в тип, имеющий встроенный <=>
. Любой библиотечный механизм, обнаруживающий наличие <=>
, выдаст здесь ложное срабатывание, и единственное решение — либо предоставить явный <=>
самостоятельно, либо сделать функцию преобразования explicit
вместо неявной.
Если бы у нас был языковой механизм, позволяющий выяснить, что конкретно вызывается t <=> u
(а такой вариант предложен в P2825), то мы могли бы добавить дополнительную проверку, которую мы выбираем <=>
только в том случае, если t <=> u
и t < u
оба жизнеспособны и вызывают один и тот же вид вещь (то есть, что они оба вызывают один и тот же operator<=>
или, если последний вызывает функцию с именем operator<
, обе функции принимают одни и те же типы параметров). Но пока этого не произошло, будьте осторожны с неявными функциями преобразования при наличии <=>
.
Но вариант operator<
подходит для varA < varB
и указано, что элементы сравниваются с <
, а не с <=>
. Нет никаких ограничений на использование synth-three-way
. OP определяет перегрузку <
с точным совпадением в аргументах Foo
, поэтому ее следует отдавать предпочтение перед переписанным кандидатом при проверке Foo
с <
. Действительно, при явном вызове operator<(varA, varB)
статическое утверждение проходит. Это не сработает при выборе operator<=>(varA, varB)
, но я не понимаю, почему переписанный кандидат будет предпочтительнее для выражения varA < varB
.
@ user17732522 Неважно, что вариант operator<
жизнеспособен, вариант operator<=>
также жизнеспособен, является лучшим кандидатом и, следовательно, предпочтительнее.
Ах, теперь я вижу. Это потому, что operator<=>
ограничен концепцией трехстороннего сравнения, а затем становится более специализированным на частичном упорядочивании шаблонов, чем кандидат operator<
, у которого нет ограничений. Более специализация в частичном упорядочении шаблонов имеет приоритет над тем, является ли кандидат переписанным или нет.
@user17732522 user17732522 Да, именно поэтому я указал это именно так - иначе <=>
просто никогда бы не вызывался. Нам нужно, чтобы <=>
предпочитали <
, если это жизнеспособно.
@Barry Спасибо за подробное объяснение. Это была настоящая головная боль.
Рекомендуется использовать
explicit operator bool
, решает ли это проблему?