Да, код фактически разрывается в C ++ 20.
Выражение Foo{} != Foo{}
имеет три кандидата в C ++ 20 (тогда как в C ++ 17 был только один):
Meta operator!=(Foo& /*this*/, const Foo&); // #1
Meta operator==(Foo& /*this*/, const Foo&); // #2
Meta operator==(const Foo&, Foo& /*this*/); // #3 - which is #2 reversed
Это происходит из новых переписанных кандидатов правил в [over.match.oper] /3.4. Все эти кандидаты являются жизнеспособными, поскольку наши аргументы Foo
не являются const
. Чтобы найти лучшего жизнеспособного кандидата, мы должны go через наших тай-брейков.
Соответствующие правила для лучшей жизнеспособной функции: [over.match.best] / 2 :
Учитывая эти определения, жизнеспособная функция F1
определена как лучшая функция, чем другая жизнеспособная функция F2
, если для всех аргументов i
, ICS<sub>i</sub>(F1)
не является худшим преобразованием последовательность чем ICS<sub>i</sub>(F2)
, а затем
- [... множество несущественных случаев для этого примера ...] или, если нет, то
- F2 - переписанный кандидат ([over.match.oper]) и F1 не
- F1 и F2 - переписанные кандидаты, а F2 - синтезированный кандидат с обратным порядком параметров, а F1 не
#2
и #3
являются переписанными кандидатами, а #3
имеет обратный порядок параметров, в то время как #1
не переписывается. Но для того, чтобы добраться до этого прерывателя связей, нам нужно сначала выполнить это начальное условие: для всех аргументов последовательности преобразования не хуже.
#1
лучше, чем #2
, потому что все последовательности преобразования одинаковы (тривиально, потому что параметры функции одинаковы), а #2
- переписанный кандидат, а #1
- нет.
Но ... обе пары #1
/ #3
и #2
/ #3
застряли в этом первом условии. В обоих случаях первый параметр имеет лучшую последовательность преобразования для #1
/ #2
, в то время как второй параметр имеет лучшую последовательность преобразования для #3
(для параметра const
значение go должно быть больше const
квалификация, поэтому она имеет худшую последовательность конвертации). Этот const
триггер заставляет нас не иметь возможности отдавать предпочтение ни одному из них.
В результате, полное разрешение перегрузки неоднозначно.
Насколько я понимаю, это работает только до тех пор, пока тип возвращаемого значения bool
.
Это не правильно. Мы безоговорочно рассматриваем переписанных и перевернутых кандидатов. У нас есть следующее правило: [over.match.oper] / 9 :
Если переписанный кандидат operator==
выбран разрешением перегрузки для оператора @
тип возвращаемого значения должен быть cv bool
То есть мы все еще рассматриваем этих кандидатов. Но если лучшим жизнеспособным кандидатом является operator==
, который возвращает, скажем, Meta
- результат в основном такой же, как если бы этот кандидат был удален.
Мы не хотим находиться в состоянии, когда разрешение перегрузки должно учитывать тип возвращаемого значения. И в любом случае тот факт, что код здесь возвращает Meta
, является несущественным - проблема также будет существовать, если он вернет bool
.
К счастью, исправить это легко:
struct Foo {
Meta operator==(const Foo&) const;
Meta operator!=(const Foo&) const;
// ^^^^^^
};
Как только вы сделаете оба оператора сравнения const
, двусмысленности больше не будет. Все параметры одинаковы, поэтому все последовательности преобразования тривиально одинаковы. #1
теперь будет бить #3
не переписанным, а #2
теперь будет бить #3
, если не будет перевернут, что делает #1
лучшим жизнеспособным кандидатом. Тот же результат, который был у нас в C ++ 17, всего несколько шагов, чтобы добраться туда.