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


11

Наступний код добре поєднує кланг-магістраль у режимі 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/isissue/1833

Моє актуальне запитання: чи насправді це неперервна зміна C ++ 20 (і чи є можливість перевантажувати оператори порівняння, щоб повернути мета-об'єкти), чи це більше ймовірність регресії в clang / gcc?


Відповіді:


5

Здається, проблема Eigen зводиться до наступного:

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&.

Чи є причина, чому власне код не міг повернути a 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- все ще немає !)
Barry

На жаль, це не вирішує мою актуальну проблему, але це було тому, що мені не вдалося надати MRE, який фактично описує мою проблему. Я прийму це, і коли зможу правильно зменшити свою проблему, я поставлю нове запитання ...
chtz

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 стовбур компілює .

Я не можу розмовляти з рештою Ейґену, оскільки не знаю коду, він дуже великий, і тому не можу поміститися в MCVE.


2
Ми потрапляємо до переліченого вами перемикача лише тоді, коли для всіх аргументів є однаково хороші перетворення. Але немає: через відсутні const, кандидати, які не повертаються, мають кращу послідовність конверсій для другого аргументу, а зворотний кандидат має кращу послідовність перетворення для першого аргументу.
Річард Сміт

@RichardSmith: Так, я говорив про таку складність. Але я не хотів насправді переглядати та читати / інтерналізувати ці правила;)
Нікол Болас,

Дійсно, я забув constпро мінімальний приклад. Я майже впевнений, що Eigen використовує constвсюди (або поза визначеннями класу, також із constпосиланнями), але мені потрібно перевірити. Я намагаюся розбити загальний механізм, який Eigen використовує на мінімальний приклад, коли знаходжу час.
chtz

-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.
Інго Йосопаїт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.