Следующий код прекрасно компилируется с clang-trunk в режиме c ++ 17, но прерывается в режиме c ++ 2a (предстоящий c ++ 20):
// Meta struct describing the result of a comparison
struct Meta {};
struct Foo {
Meta operator==(const Foo&) {return Meta{};}
Meta operator!=(const Foo&) {return Meta{};}
};
int main()
{
Meta res = (Foo{} != Foo{});
}
Он также прекрасно компилируется с gcc-trunk или clang-9.0.0: https://godbolt.org/z/8GGT78.
Ошибка с clang-trunk и -std=c++2a
:
<source>:12:19: error: use of overloaded operator '!=' is ambiguous (with operand types 'Foo' and 'Foo')
Meta res = (f != g);
~ ^ ~
<source>:6:10: note: candidate function
Meta operator!=(const Foo&) {return Meta{};}
^
<source>:5:10: note: candidate function
Meta operator==(const Foo&) {return Meta{};}
^
<source>:5:10: note: candidate function (with reversed parameter order)
Я понимаю, что C ++ 20 сделает возможной только перегрузку, operator==
и компилятор автоматически сгенерирует operator!=
отрицание результата operator==
. Насколько я понимаю, это работает только до тех пор, пока возвращается тип bool
.
Источник проблемы заключается в том, что в Эйгене мы объявляем набор операторов ==
, !=
, <
, ... между Array
объектами илиArray
и скалярами, которые возвращают (выражение) массив bool
(который затем может быть доступен поэлементен, или использоваться в противном случае ). Например,
#include <Eigen/Core>
int main()
{
Eigen::ArrayXd a(10);
a.setRandom();
return (a != 0.0).any();
}
В отличие от моего примера выше, это даже не работает с gcc-trunk: https://godbolt.org/z/RWktKs . Я еще не успел свести это к не-собственному примеру, который терпит неудачу как в clang-trunk, так и в gcc-trunk (пример вверху довольно упрощен).
Связанный отчет о проблеме: https://gitlab.com/libeigen/eigen/issues/1833
Мой актуальный вопрос: действительно ли это серьезное изменение в C ++ 20 (и есть ли возможность перегрузить операторы сравнения для возврата мета-объектов) или это более вероятно регрессия в clang / gcc?
Ответы:
Собственная проблема, по-видимому, сводится к следующему:
Два кандидата на выражение
operator==(const Scalar&, const Derived&)
Base<X>::operator!=(const Scalar&) const
Согласно [over.match.funcs] / 4 , поскольку
operator!=
не было импортировано в область действияX
с помощью объявления-использования , тип неявного параметра объекта для # 2:const Base<X>&
. В результате # 1 имеет лучшую неявную последовательность преобразования для этого аргумента (точное совпадение, а не преобразование из производной в основание). Выбор # 1 делает программу некорректной.Возможные исправления:
using Base::operator!=;
вDerived
илиoperator==
чтобы взятьconst Base&
вместоconst Derived&
.источник
bool
ихoperator==
? Потому что это, кажется, единственная причина, по которой код плохо сформирован в соответствии с новыми правилами.operator==(Array, Scalar)
поэлементное сравнение и возвращаетArray
изbool
. Вы не можете превратить это в,bool
не нарушая все остальное.operator==
не должны были влиять на существующий код, но в этом случае они влияют, потому что проверкаbool
возвращаемого значения не является частью выбора кандидатов на переписывание.Да, код фактически ломается в C ++ 20.
Выражение
Foo{} != Foo{}
имеет три кандидата в C ++ 20 (тогда как в C ++ 17 был только один):Это происходит из новых переписанных правил- кандидатов в [over.match.oper] /3.4 . Все эти кандидаты являются жизнеспособными, поскольку наши
Foo
аргументы не являютсяconst
. Чтобы найти лучшего жизнеспособного кандидата, мы должны пройти через наши тай-брейки.Соответствующие правила для лучшей жизнеспособной функции, из [over.match.best] / 2 :
#2
и#3
переписаны кандидаты, и#3
имеет обратный порядок параметров, в то время#1
как не переписан. Но для того, чтобы добраться до разрыва связей, нам нужно сначала пройти через это начальное условие: для всех аргументов последовательности преобразования не хуже.#1
Это лучше, чем#2
потому, что все последовательности преобразования одинаковы (тривиально, потому что параметры функции одинаковы) и#2
является переписанным кандидатом, а#1
не -.Но ... обе пары
#1
/#3
и#2
/#3
застряли на этом первом условии. В обоих случаях первый параметр имеет лучшую последовательность преобразования для#1
/,#2
а второй параметр имеет лучшую последовательность преобразования для#3
(параметр, которыйconst
должен пройти дополнительнуюconst
квалификацию, поэтому он имеет худшую последовательность преобразования). Этаconst
триггер заставляет нас не иметь возможности отдавать предпочтение ни одному из них.В результате, полное разрешение перегрузки неоднозначно.
Это не правильно. Мы безоговорочно рассматриваем переписанных и перевернутых кандидатов. У нас есть правило из [over.match.oper] / 9 :
То есть мы еще рассматриваем этих кандидатов. Но если лучшим жизнеспособным кандидатом является
operator==
тот, который возвращает, скажем,Meta
- результат в основном такой же, как если бы этот кандидат был удален.Мы не хотели находиться в состоянии, когда разрешение перегрузки должно учитывать тип возвращаемого значения. И в любом случае тот факт, что код здесь возвращается, не
Meta
имеет значения - проблема также будет существовать, если он вернетсяbool
.К счастью, исправить это легко:
Как только вы сделаете оба оператора сравнения
const
, двусмысленности больше не будет. Все параметры одинаковы, поэтому все последовательности преобразования тривиально одинаковы.#1
теперь будет биться#3
не переписанным и#2
теперь будет биться#3
не обращенным - что делает#1
лучшего жизнеспособного кандидата. Тот же результат, который был у нас в C ++ 17, всего несколько шагов, чтобы добраться туда.источник
==
а тип возврата выбранной функции - нетbool
. Но этот выбор не происходит во время самого разрешения перегрузки.cv bool
(и до этого изменения требовалось контекстное преобразование вbool
- все еще нет!
)[over.match.best] / 2 показывает, как приоритеты имеют допустимые перегрузки в наборе. Раздел 2.8 говорит нам, что
F1
лучше, чемF2
если (среди многих других вещей):Пример там показывает явный
operator<
вызов, даже еслиoperator<=>
он есть.И [over.match.oper] /3.4.3 говорит нам, что кандидатура
operator==
в этом случае является переписанным кандидатом.Однако ваши операторы забывают одну важную вещь: они должны быть
const
функциями. И не делая их,const
в игру вступают более ранние аспекты разрешения перегрузки. Ни одна из функций является точным соответствием, так как не-const
-До-const
преобразования должны произойти для различных аргументов. Это вызывает неоднозначность в вопросе.Как только вы их сделаете
const
, Clang trunk компилирует .Я не могу говорить с остальной частью Eigen, так как я не знаю код, он очень большой, и поэтому не может вписаться в MCVE.
источник
const
обратные кандидаты имеют лучшую последовательность преобразования для второго аргумента, а обратный кандидат имеет лучшую последовательность преобразования для первого аргумента.const
в минимальном примере. Я почти уверен, что Eigen используетconst
везде (или вне определений классов, также соconst
ссылками), но мне нужно проверить. Я пытаюсь разбить общий механизм, который использует Эйген, до минимального примера, когда я найду время.У нас есть похожие проблемы с нашими заголовочными файлами Goopax. Компиляция следующего с clang-10 и -std = c ++ 2a приводит к ошибке компилятора.
Предоставление этих дополнительных операторов, кажется, решает проблему:
источник
a == 0
скомпилировали ?gpu_bool gpu_type<T>::operator==(T a) const;
аgpu_bool gpu_type<T>::operator!=(T a) const;
с C ++ - 17 это работает нормально. Но теперь с clang-10 и C ++ - 20, они больше не найдены, и вместо этого компилятор пытается генерировать свои собственные операторы путем замены аргументов, и это терпит неудачу, потому что возвращаемый тип неbool
.