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?


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

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


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


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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.