Поведение с ++ 20 нарушает существующий код с помощью оператора равенства?

Jan 10 2021

Я столкнулся с этим при отладке этого вопроса .

Я урезал его полностью, чтобы использовать только операторы Boost :

  1. Обозреватель компилятора C ++ 17 C ++ 20

    #include <boost/operators.hpp>
    
    struct F : boost::totally_ordered1<F, boost::totally_ordered2<F, int>> {
        /*implicit*/ F(int t_) : t(t_) {}
        bool operator==(F const& o) const { return t == o.t; }
        bool operator< (F const& o) const { return t <  o.t; }
      private: int t;
    };
    
    int main() {
        #pragma GCC diagnostic ignored "-Wunused"
        F { 42 } == F{ 42 }; // OKAY
        42 == F{42};         // C++17 OK, C++20 infinite recursion
        F { 42 } == 42;      // C++17 OK, C++20 infinite recursion
    }
    

    Эта программа компилируется и отлично работает с C ++ 17 (включен ubsan / asan) как в GCC, так и в Clang.

  2. Когда вы меняете неявный конструктор на explicit, очевидно, что проблемные строки больше не компилируются на C ++ 17.

Удивительно, но обе версии компилируются на C ++ 20 ( v1 и v2 ) , но они приводят к бесконечной рекурсии (сбой или жесткий цикл, в зависимости от уровня оптимизации) в двух строках, которые не будут компилироваться на C ++ 17.

Очевидно, что подобная скрытая ошибка, появляющаяся при обновлении до C ++ 20, вызывает беспокойство.

Вопросов:

  • Соответствует ли это поведение С ++ 20 (я так ожидаю)
  • Что именно мешает? Я подозреваю, что это может быть из-за поддержки нового C ++ 20 «оператора космического корабля», но не понимаю, как это меняет поведение этого кода.

Ответы

81 Barry Jan 10 2021 at 07:27

Действительно, C ++ 20, к сожалению, делает этот код бесконечно рекурсивным.

Вот сокращенный пример:

struct F {
    /*implicit*/ F(int t_) : t(t_) {}

    // member: #1
    bool operator==(F const& o) const { return t == o.t; }

    // non-member: #2
    friend bool operator==(const int& y, const F& x) { return x == y; }

private:
    int t;
};

Давайте просто посмотрим 42 == F{42}.

В C ++ 17 у нас был только один кандидат: кандидат, не являющийся членом ( #2), поэтому мы его выбираем. Его тело x == y,, само по себе имеет только одного кандидата: член кандидата ( #1), который подразумевает неявное преобразование yв F. А затем этот кандидат в члены сравнивает два целых члена, и это совершенно нормально.

В C ++ 20 у исходного выражения 42 == F{42}теперь есть два кандидата: как кандидат, не являющийся членом ( #2), как и раньше, так и теперь также обратный кандидат в члены ( #1обратный). #2является лучшим совпадением - мы точно сопоставляем оба аргумента вместо того, чтобы вызывать преобразование, поэтому он выбран.

Теперь, однако, x == yтеперь есть два кандидата: снова кандидат в члены ( #1), но также и обратный кандидат, не являющийся членом ( #2обратный). #2снова является лучшим совпадением по той же причине, что и раньше: никаких преобразований не требуется. Поэтому y == xвместо этого мы оцениваем . Бесконечная рекурсия.

Неотмененные кандидаты предпочтительнее обратных кандидатов, но только в качестве разрешения на ничью. Лучшая последовательность преобразования всегда первая.


Ладно, отлично, как мы можем это исправить? Самый простой вариант - полностью удалить кандидата, не являющегося членом:

struct F {
    /*implicit*/ F(int t_) : t(t_) {}

    bool operator==(F const& o) const { return t == o.t; }

private:
    int t;
};

42 == F{42}здесь оценивается как F{42}.operator==(42), что отлично работает.

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

struct F {
    /*implicit*/ F(int t_) : t(t_) {}
    bool operator==(F const& o) const { return t == o.t; }
    bool operator==(int i) const { return t == i; }
    friend bool operator==(const int& y, const F& x) { return x == y; }

private:
    int t;
};

Это заставляет по- 42 == F{42}прежнему выбирать x == yкандидата, не являющегося членом, но теперь в теле предпочтение будет отдано кандидату-члену, который затем выполняет нормальное равенство.

Эта последняя версия также может удалить кандидата, не являющегося членом. Следующее также работает без рекурсии для всех тестовых случаев (и это то, как я буду писать сравнения на C ++ 20 в будущем):

struct F {
    /*implicit*/ F(int t_) : t(t_) {}
    bool operator==(F const& o) const { return t == o.t; }
    bool operator==(int i) const { return t == i; }

private:
    int t;
};