Std::variant::operator< неожиданный вызов неявного преобразования bool. Зависит от стандартов

Я наблюдаю неожиданное поведение при использовании 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

Обратите внимание, что утверждение начинает проходить, когда выполняется одно из следующих условий:

  • используйте C++17 вместо C++20
  • вместо этого используйте оператор трехстороннего сравнения, свободную функцию, меньше оператора
  • не определять неявное преобразование в оператор bool
  • пометка оператора преобразования bool как явного

Все компиляторы ведут себя одинаково.

Рекомендуется использовать explicit operator bool, решает ли это проблему?

Daniel 26.07.2024 13:00

Почему у тебя operator bool вместо explicit operator bool?

Eljay 26.07.2024 13:25

Так что если сделать это explicit operator bool, это исправит ситуацию. Этот объект очень старый, поэтому он не был указан ранее.

Plog 26.07.2024 13:41

Получил полную копию constexpr для всех основных компиляторов: godbolt.org/z/34fsqY5dn

Marek R 26.07.2024 13:46

@MarekR, спасибо, что так много вникал в это

Plog 26.07.2024 13:47

Re: «Оператор приведения bool» — это оператор преобразования; приведение — это то, что вы пишете в исходном коде, чтобы сообщить компилятору выполнить преобразование.

Pete Becker 26.07.2024 14:14

Связано: stackoverflow.com/q/78346768/11910702

Erel 28.07.2024 02:27
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
7
153
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хех, я точно знал, что это будет за код, когда прочитал заголовок. Я не могу найти подходящего объекта для обмана, поэтому постараюсь сделать это каноническим ответом.

С++17

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

С++20

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

Мы можем пройти через различное поведение. Я предполагаю, что мы всегда предоставляем ровно один из < или <=>:

стандартный предоставить оператор bool какой заказ что происходит С++17 нет < по сравнению с < С++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 26.07.2024 16:54

@ user17732522 Неважно, что вариант operator< жизнеспособен, вариант operator<=> также жизнеспособен, является лучшим кандидатом и, следовательно, предпочтительнее.

Barry 26.07.2024 17:01

Ах, теперь я вижу. Это потому, что operator<=> ограничен концепцией трехстороннего сравнения, а затем становится более специализированным на частичном упорядочивании шаблонов, чем кандидат operator<, у которого нет ограничений. Более специализация в частичном упорядочении шаблонов имеет приоритет над тем, является ли кандидат переписанным или нет.

user17732522 26.07.2024 17:03

@user17732522 user17732522 Да, именно поэтому я указал это именно так - иначе <=> просто никогда бы не вызывался. Нам нужно, чтобы <=> предпочитали <, если это жизнеспособно.

Barry 26.07.2024 17:06

@Barry Спасибо за подробное объяснение. Это была настоящая головная боль.

Plog 26.07.2024 18:06

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