Я наблюдаю неожиданное поведение при использовании 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, решает ли это проблему?