Przerwanie zmiany w C ++ 20 czy regresja w clang-trunk / gcc-trunk podczas przeciążania porównania równości z wartością logiczną inną niż logiczna?

11

Poniższy kod kompiluje się dobrze z clang-trunk w trybie c ++ 17, ale psuje się w trybie c ++ 2a (nadchodzące 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{});
}

Kompiluje się również dobrze z gcc-trunk lub clang-9.0.0: https://godbolt.org/z/8GGT78

Błąd z clang-trunk i -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)

Rozumiem, że C ++ 20 pozwoli jedynie na przeciążenie, operator==a kompilator wygeneruje automatycznie operator!=, negując wynik operator==. O ile rozumiem, działa to tylko tak długo, jak typ zwrotu jest bool.

Źródłem problemu jest to, że w Eigen deklarujemy zestaw operatorów ==, !=, <, ... między Arrayobiektami lub Arrayi skalary, które obie strony (wyraz) tablicę bool(które następnie można uzyskać element mądry lub wykorzystywane w inny sposób ). Na przykład,

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

W przeciwieństwie do mojego powyższego przykładu, nawet nie działa z gcc-trunk: https://godbolt.org/z/RWktKs . Nie udało mi się jeszcze zredukować tego do przykładu innego niż Eigen, który zawodzi zarówno w przypadku clang-trunk, jak i gcc-trunk (przykład na górze jest dość uproszczony).

Raport dotyczący pokrewnego problemu: https://gitlab.com/libeigen/eigen/issues/1833

Moje aktualne pytanie: czy to rzeczywiście przełomowa zmiana w C ++ 20 (i czy istnieje możliwość przeciążenia operatorów porównania, aby zwrócić obiekty meta), czy może jest to regresja w clang / gcc?

chtz
źródło

Odpowiedzi:

5

Wydaje się, że problem Eigen ogranicza się do:

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;
}

Dwoma kandydatami do wyrażenia są

  1. przepisany kandydat z operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Dla [over.match.funcs] / 4 , ponieważ operator!=nie został zaimportowany do zakresu Xprzez deklarację użycia , typem niejawnego parametru obiektu dla # 2 jest const Base<X>&. W rezultacie numer 1 ma lepszą domyślną sekwencję konwersji dla tego argumentu (dokładne dopasowanie, a nie konwersja pochodna do podstawy). Wybranie # 1 powoduje, że program jest źle sformułowany.

Możliwe poprawki:

  • Dodaj using Base::operator!=;do Derivedlub
  • Zmień, operator==aby wziąć const Base&zamiast zamiast const Derived&.
TC
źródło
Czy istnieje powód, dla którego rzeczywisty kod nie mógł zwrócić boolz nich operator==? Ponieważ wydaje się, że jest to jedyny powód, dla którego kod jest źle sformułowany zgodnie z nowymi zasadami.
Nicol Bolas,
4
Rzeczywisty kod obejmuje przygotowanie operator==(Array, Scalar)że robi element mądry porównania i zwracają Arrayod bool. Nie można tego zmienić boolbez zepsucia wszystkiego innego.
TC
2
To wygląda trochę jak wada standardu. Reguły przepisywania operator==nie miały wpływać na istniejący kod, ale mają one wpływ w tym przypadku, ponieważ sprawdzenie boolwartości zwracanej nie jest częścią wybierania kandydatów do przepisania.
Nicol Bolas,
2
@NicolBolas: Zgodnie z ogólną zasadą sprawdza się, czy można coś zrobić ( np. Wywołać operatora), a nie czy należy , aby uniknąć wprowadzania zmian implementacyjnych po cichu wpływających na interpretację innego kodu. Okazuje się, że przepisane porównania psują wiele rzeczy, ale głównie rzeczy, które były już wątpliwe i łatwe do naprawienia. Tak więc, na dobre i na złe, reguły te zostały przyjęte.
Davis Herring
Wow, wielkie dzięki, myślę, że twoje rozwiązanie rozwiąże nasz problem (w tej chwili nie mam czasu na instalację gcc / clang trunk z rozsądnym wysiłkiem, więc po prostu sprawdzę, czy to nie psuje nic do najnowszych stabilnych wersji kompilatora ).
Chtz
11

Tak, kod faktycznie psuje się w C ++ 20.

W Foo{} != Foo{}C ++ 20 wyrażenie ma trzech kandydatów (podczas gdy w C ++ 17 był tylko jeden):

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

Wynika to z nowych przepisanych reguł dla kandydatów w [over.match.oper] /3.4 . Wszyscy ci kandydaci są wykonalni, ponieważ nasze Fooargumenty nie są const. Aby znaleźć najlepszego realnego kandydata, musimy przejrzeć naszych rozstrzygających.

Odpowiednie zasady dla najlepszej wykonalnej funkcji są z [over.match.best] / 2 :

Biorąc pod uwagę te definicje, realną funkcja F1jest zdefiniowana jako funkcja lepiej niż innym realnej funkcji F2, jeśli dla wszystkich argumentów i, nie jest gorszy niż sekwencja konwersji , a następnie ICSi(F1)ICSi(F2)

  • [... wiele nieistotnych przypadków dla tego przykładu ...] lub, jeśli nie to, to
  • F2 to przepisany kandydat ([over.match.oper]), a F1 nie
  • F1 i F2 to przepisani kandydaci, a F2 to zsyntetyzowany kandydat o odwróconej kolejności parametrów, a F1 nie

#2i #3są przepisanymi kandydatami i #3mają odwróconą kolejność parametrów, ale #1nie są przepisane. Ale aby dostać się do tego rozstrzygającego, musimy najpierw przejść przez ten warunek początkowy: dla wszystkich argumentów sekwencje konwersji nie są gorsze.

#1jest lepszy niż #2ponieważ wszystkie sekwencje konwersji są takie same (trywialnie, ponieważ parametry funkcji są takie same) i #2jest przepisanym kandydatem, podczas gdy #1nie jest.

Ale ... obie pary #1/ #3i #2/ #3 utkną na pierwszym warunku. W obu przypadkach pierwszy parametr ma lepszą sekwencję konwersji dla #1/ #2podczas gdy drugi parametr ma lepszą sekwencję konwersji dla #3(parametr, który constmusi przejść dodatkową constkwalifikację, więc ma gorszą sekwencję konwersji). Ten constflip-flop powoduje, że nie jesteśmy w stanie preferować żadnego z nich.

W rezultacie cała rozdzielczość przeciążenia jest niejednoznaczna.

O ile rozumiem, działa to tylko tak długo, jak typ zwrotu jest bool.

To nie jest poprawne Bezwarunkowo rozważamy przepisanie i zmianę kandydatów. Zasada jest taka, że ​​z [over.match.oper] / 9 :

W przypadku operator==wybrania przepisanego kandydata na podstawie rozdzielczości przeciążenia dla operatora @, typem zwrotu jest cv bool

Oznacza to, że nadal rozważamy tych kandydatów. Ale jeśli najlepszym opłacalnym kandydatem jest taki, operator==który zwraca, powiedzmy Meta- wynik jest zasadniczo taki sam, jak gdyby ten kandydat został usunięty.

Zrobiliśmy nie chcą być w stanie, w którym rozdzielczość przeciążenie musiałaby wziąć pod uwagę typ zwracany. W każdym razie fakt, że kod tutaj zwraca, Metajest nieistotny - problem istniałby również, gdyby został zwrócony bool.


Na szczęście poprawka tutaj jest łatwa:

struct Foo {
    Meta operator==(const Foo&) const;
    Meta operator!=(const Foo&) const;
    //                         ^^^^^^
};

Po utworzeniu obu operatorów porównania constnie ma już dwuznaczności. Wszystkie parametry są takie same, więc wszystkie sekwencje konwersji są banalnie takie same. #1pokonałby teraz #3nie przez przepisanie i pokonałby #2teraz, że nie zostałby #3cofnięty - co czyni #1najlepszego możliwego kandydata. Ten sam wynik, który mieliśmy w C ++ 17, tylko kilka kroków, aby się tam dostać.

Barry
źródło
Nie chcieliśmy być w stanie, w którym rozdzielczość przeciążenia musiałaby wziąć pod uwagę typ zwrotu. ” Żeby było jasne, podczas gdy sama rozdzielczość przeciążenia nie bierze pod uwagę typu zwrotu, robią to kolejne przepisane operacje . Jeden kod jest źle sformułowany, jeśli rozdzielczość przeciążenia wybrałaby przepisany kod, ==a typ powrotu wybranej funkcji nie jest bool. Ale to ubijanie nie występuje podczas rozwiązywania problemu przeciążenia.
Nicol Bolas
Jest właściwie źle sformułowany, jeśli typ zwrotu jest czymś, co nie obsługuje operatora! ...
Chris Dodd
1
@ChrisDodd Nie, musi być dokładnie cv bool(i przed tą zmianą wymagano konwersji kontekstowej na bool- wciąż nie !)
Barry
Niestety, to nie rozwiązuje mojego rzeczywistego problemu, ale dlatego, że nie dostarczyłem MRE, który faktycznie opisuje mój problem. Ja przyjmuję to i kiedy jestem w stanie zmniejszyć mój problem właściwie Poproszę nowe pytanie ...
CHTZ
2
Wygląda na to, że poprawną redukcją oryginalnego wydania jest gcc.godbolt.org/z/tFy4qz
TC
5

[over.match.best] / 2 pokazuje, w jaki sposób priorytetowe są prawidłowe przeciążenia w zestawie. Sekcja 2.8 mówi nam, że F1jest lepiej niż F2gdyby (wśród wielu innych rzeczy):

F2jest przepisanym kandydatem ([over.match.oper]) i F1nie jest

Przykład pokazuje wyraźne operator<wywołanie, mimo że operator<=>istnieje.

A [over.match.oper] /3.4.3 mówi nam, że kandydatura operator==w tych okolicznościach jest przepisanym kandydatem.

Jednak operatorzy zapominają o jednej kluczowej rzeczy: powinny to być constfunkcje. A ich brak constpowoduje, że w grę wchodzą wcześniejsze aspekty rozwiązania problemu przeciążenia. Żadna funkcja jest dokładne dopasowanie, jak nie- const-to- constkonwersje muszą się zdarzyć w różnych argumentów. To powoduje sporą dwuznaczność.

Po dokonaniu ich const, Clang kompiluje tułowia .

Nie mogę rozmawiać z resztą Eigen, ponieważ nie znam kodu, jest bardzo duży, a zatem nie mieści się w MCVE.

Nicol Bolas
źródło
2
Dojedziemy do wymienionego przez ciebie rozstrzygnięcia tylko wtedy, gdy dla wszystkich argumentów są równie dobre konwersje. Ale nie ma: z powodu brakujących constnieodwróconych kandydatów ma lepszą sekwencję konwersji dla drugiego argumentu, a odwrócony kandydat ma lepszą sekwencję konwersji dla pierwszego argumentu.
Richard Smith
@RichardSmith: Tak, o takiej złożoności mówiłem. Ale nie chciałem musieć przejść przez te zasady i czytać / internalizować;)
Nicol Bolas,
Rzeczywiście zapomniałem constw minimalnym przykładzie. Jestem pewien, że Eigen używa constwszędzie (lub poza definicjami klas, także z constreferencjami), ale muszę to sprawdzić. Kiedy znajdę czas, staram się rozbić ogólny mechanizm wykorzystywany przez Eigen na minimalny przykład.
chtz
-1

Mamy podobne problemy z naszymi plikami nagłówkowymi Goopax. Kompilowanie poniższych z clang-10 i -std = c ++ 2a powoduje błąd kompilatora.

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);
}

Zapewnienie tych dodatkowych operatorów wydaje się rozwiązać problem:

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);
};
Ingo Josopait
źródło
1
Czy nie było to coś, co przydałoby się wcześniej? W przeciwnym razie, jak by a == 0się skompilował ?
Nicol Bolas,
To naprawdę nie jest podobny problem. Jak zauważył Nicol, nie skompilowało się to już w C ++ 17. Nadal nie kompiluje się w C ++ 20, tylko z innego powodu.
Barry
Zapomniałem wspomnieć: Zapewniamy również operatorów członkowskich: gpu_bool gpu_type<T>::operator==(T a) const;a gpu_bool gpu_type<T>::operator!=(T a) const;przy C ++ - 17 działa to dobrze. Ale teraz w przypadku clang-10 i C ++ - 20 nie można ich już znaleźć, a zamiast tego kompilator próbuje wygenerować własne operatory poprzez zamianę argumentów, ale to się nie udaje, ponieważ typ zwracany nie jest bool.
Ingo Josopait