Поведение с ++ 20 нарушает существующий код с помощью оператора равенства?
Я столкнулся с этим при отладке этого вопроса .
Я урезал его полностью, чтобы использовать только операторы Boost :
Обозреватель компилятора 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.
Когда вы меняете неявный конструктор на
explicit
, очевидно, что проблемные строки больше не компилируются на C ++ 17.
Удивительно, но обе версии компилируются на C ++ 20 ( v1 и v2 ) , но они приводят к бесконечной рекурсии (сбой или жесткий цикл, в зависимости от уровня оптимизации) в двух строках, которые не будут компилироваться на C ++ 17.
Очевидно, что подобная скрытая ошибка, появляющаяся при обновлении до C ++ 20, вызывает беспокойство.
Вопросов:
- Соответствует ли это поведение С ++ 20 (я так ожидаю)
- Что именно мешает? Я подозреваю, что это может быть из-за поддержки нового C ++ 20 «оператора космического корабля», но не понимаю, как это меняет поведение этого кода.
Ответы
Действительно, 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;
};