Прерывание изменений в C ++ 20 или регрессия в clang-trunk / gcc-trunk при перегрузке сравнения равенства с небулевым возвращаемым значением?

11

Следующий код прекрасно компилируется с 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?

ЧТЗ
источник
Связанный: stackoverflow.com/questions/58319928/…
chtz

Ответы:

5

Собственная проблема, по-видимому, сводится к следующему:

using Scalar = double;

template<class Derived>
struct Base {
    friend inline int operator==(const Scalar&, const Derived&) { return 1; }
    int operator!=(const Scalar&) const;
};

struct X : Base<X> {};

int main() {
    X{} != 0.0;
}

Два кандидата на выражение

  1. переписанный кандидат от operator==(const Scalar&, const Derived&)
  2. 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&.
TC
источник
Есть ли причина, по которой фактический код не может вернуть boolих operator==? Потому что это, кажется, единственная причина, по которой код плохо сформирован в соответствии с новыми правилами.
Никол Болас
4
Фактический код включает в себя operator==(Array, Scalar)поэлементное сравнение и возвращает Arrayиз bool. Вы не можете превратить это в, boolне нарушая все остальное.
ТК
2
Это похоже на дефект в стандарте. Правила переписывания operator==не должны были влиять на существующий код, но в этом случае они влияют, потому что проверка boolвозвращаемого значения не является частью выбора кандидатов на переписывание.
Никол Болас
2
@NicolBolas: Общий принцип, которому следует следовать, заключается в том, что проверка заключается в том, можете ли вы что-то делать ( например , вызывать оператор), а не должны ли вы , чтобы изменения реализации не сказывались на интерпретации другого кода. Оказывается, что переписанные сравнения ломают много вещей, но в основном вещи, которые уже были сомнительными и их легко исправить. Так или иначе, эти правила были приняты в любом случае.
Дэвис Херринг
Вау, спасибо большое, я думаю, что ваше решение решит нашу проблему (у меня нет времени, чтобы установить gcc / clang trunk с разумными усилиями в данный момент, так что я просто проверю, не нарушает ли это что-либо до последних стабильных версий компилятора ).
ЧТЗ
11

Да, код фактически ломается в 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 . Чтобы найти лучшего жизнеспособного кандидата, мы должны пройти через наши тай-брейки.

Соответствующие правила для лучшей жизнеспособной функции, из [over.match.best] / 2 :

Учитывая эти определения, жизнеспособная функция F1определяется как лучшая функция, чем другая жизнеспособная функция, F2если для всех аргументов i,ICSi(F1) не хуже , чем последовательность преобразования , а затем ICSi(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должен пройти дополнительную 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, всего несколько шагов, чтобы добраться туда.

Барри
источник
« Мы не хотели находиться в состоянии, в котором при разрешении перегрузки необходимо учитывать тип возвращаемого значения. » Просто для ясности, хотя само разрешение при перегрузке не учитывает тип возвращаемого значения, это делают последующие переписанные операции . У кого-то неправильный код, если разрешение перегрузки выберет переписанный текст, ==а тип возврата выбранной функции - нет bool. Но этот выбор не происходит во время самого разрешения перегрузки.
Николь Болас
На самом деле, он плохо сформирован, если тип возвращаемого значения не поддерживает оператор! ...
Крис Додд,
1
@ChrisDodd Нет, это должно быть точно cv bool(и до этого изменения требовалось контекстное преобразование в bool- все еще нет !)
Барри
К сожалению, это не решает мою реальную проблему, но это было потому, что я не смог предоставить MRE, которое фактически описывает мою проблему. Я принимаю это , и когда я могу уменьшить мою проблему правильно , я задать новый вопрос ...
ЧТЗ
2
Похоже, правильное сокращение оригинальной проблемы - gcc.godbolt.org/z/tFy4qz
TC
5

[over.match.best] / 2 показывает, как приоритеты имеют допустимые перегрузки в наборе. Раздел 2.8 говорит нам, что F1лучше, чем F2если (среди многих других вещей):

F2является переписанным кандидатом ([over.match.oper]) и F1не является

Пример там показывает явный operator<вызов, даже если operator<=>он есть.

И [over.match.oper] /3.4.3 говорит нам, что кандидатура operator==в этом случае является переписанным кандидатом.

Однако ваши операторы забывают одну важную вещь: они должны быть constфункциями. И не делая их, constв игру вступают более ранние аспекты разрешения перегрузки. Ни одна из функций является точным соответствием, так как не- const-До-const преобразования должны произойти для различных аргументов. Это вызывает неоднозначность в вопросе.

Как только вы их сделаете const, Clang trunk компилирует .

Я не могу говорить с остальной частью Eigen, так как я не знаю код, он очень большой, и поэтому не может вписаться в MCVE.

Николь Болас
источник
2
Мы попадаем к списку связей, который вы перечислили, только если есть одинаково хорошие преобразования для всех аргументов. Но это не так: из-за отсутствия constобратные кандидаты имеют лучшую последовательность преобразования для второго аргумента, а обратный кандидат имеет лучшую последовательность преобразования для первого аргумента.
Ричард Смит
@RichardSmith: Да, это была та сложность, о которой я говорил. Но я не хотел на самом деле проходить и читать / усваивать эти правила;)
Никол Болас
Действительно, я забыл constв минимальном примере. Я почти уверен, что Eigen использует constвезде (или вне определений классов, также со constссылками), но мне нужно проверить. Я пытаюсь разбить общий механизм, который использует Эйген, до минимального примера, когда я найду время.
ЧТЗ
-1

У нас есть похожие проблемы с нашими заголовочными файлами Goopax. Компиляция следующего с clang-10 и -std = c ++ 2a приводит к ошибке компилятора.

template<typename T> class gpu_type;

using gpu_bool     = gpu_type<bool>;
using gpu_int      = gpu_type<int>;

template<typename T>
class gpu_type
{
  friend inline gpu_bool operator==(T a, const gpu_type& b);
  friend inline gpu_bool operator!=(T a, const gpu_type& b);
};

int main()
{
  gpu_int a;
  gpu_bool b = (a == 0);
}

Предоставление этих дополнительных операторов, кажется, решает проблему:

template<typename T>
class gpu_type
{
  ...
  friend inline gpu_bool operator==(const gpu_type& b, T a);
  friend inline gpu_bool operator!=(const gpu_type& b, T a);
};
Инго Йосопайт
источник
1
Разве это не было бы полезно сделать заранее? Иначе как бы a == 0скомпилировали ?
Никол Болас
Это не совсем похожая проблема. Как отметил Николь, это уже не компилируется в C ++ 17. Он продолжает не компилироваться в C ++ 20, просто по другой причине.
Барри
Я забыл упомянуть: мы также предоставляем операторы-члены: 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 .
Инго Йосопайт