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

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

Он также прекрасно компилируется с помощью g cc -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.

Источник проблемы заключается в том, что в Eigen мы объявляем набор операторов ==, !=, <, ... между Array объектами или Array и скалярами, которые возвращают (выражение) массива bool (к которому затем можно обращаться поэлементно или использовать иным образом). Например,

#include <Eigen/Core>
int main()
{
  Eigen::ArrayXd a(10);
  a.setRandom();
  return (a != 0.0).any();
}

В отличие от моего примера выше, это даже не работает с g cc -trunk: https://godbolt.org/z/RWktKs. Мне еще не удалось свести это к не-собственному примеру, который завершается неудачей как в clang-trunk, так и в g cc -trunk (пример в верхней части довольно упрощен).

Отчет по связанной проблеме: https://gitlab.com/libeigen/eigen/issues/1833

Мой актуальный вопрос: действительно ли это критическое изменение в C ++ 20 (и есть ли возможность перегрузить операторы сравнения для возврата мета-объектов), или это скорее регресс в clang / g cc?

Ответы [ 4 ]

11 голосов
/ 06 марта 2020

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

5 голосов
/ 07 марта 2020

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

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 с помощью с использованием -declaration , тип неявного параметра объекта для # 2 - const Base<X>&. В результате # 1 имеет лучшую неявную последовательность преобразования для этого аргумента (точное совпадение, а не преобразование из производной в основание). Выбор # 1 делает программу некорректной.

Возможные исправления:

  • Добавление using Base::operator!=; в Derived или
  • Изменение operator== взять const Base& вместо const Derived&.
5 голосов
/ 06 марта 2020

[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 -to- const. Это вызывает неоднозначность в вопросе.

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

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

0 голосов
/ 08 марта 2020

У нас похожие проблемы с нашими заголовочными файлами 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);
};
...