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 Array
obiektami lub Array
i 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:
Wydaje się, że problem Eigen ogranicza się do:
Dwoma kandydatami do wyrażenia są
operator==(const Scalar&, const Derived&)
Base<X>::operator!=(const Scalar&) const
Dla [over.match.funcs] / 4 , ponieważ
operator!=
nie został zaimportowany do zakresuX
przez deklarację użycia , typem niejawnego parametru obiektu dla # 2 jestconst 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:
using Base::operator!=;
doDerived
luboperator==
aby wziąćconst Base&
zamiast zamiastconst Derived&
.źródło
bool
z nichoperator==
? Ponieważ wydaje się, że jest to jedyny powód, dla którego kod jest źle sformułowany zgodnie z nowymi zasadami.operator==(Array, Scalar)
że robi element mądry porównania i zwracająArray
odbool
. Nie można tego zmienićbool
bez zepsucia wszystkiego innego.operator==
nie miały wpływać na istniejący kod, ale mają one wpływ w tym przypadku, ponieważ sprawdzeniebool
wartości zwracanej nie jest częścią wybierania kandydatów do przepisania.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):Wynika to z nowych przepisanych reguł dla kandydatów w [over.match.oper] /3.4 . Wszyscy ci kandydaci są wykonalni, ponieważ nasze
Foo
argumenty 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 :
#2
i#3
są przepisanymi kandydatami i#3
mają odwróconą kolejność parametrów, ale#1
nie 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.#1
jest lepszy niż#2
ponieważ wszystkie sekwencje konwersji są takie same (trywialnie, ponieważ parametry funkcji są takie same) i#2
jest przepisanym kandydatem, podczas gdy#1
nie jest.Ale ... obie pary
#1
/#3
i#2
/#3
utkną na pierwszym warunku. W obu przypadkach pierwszy parametr ma lepszą sekwencję konwersji dla#1
/#2
podczas gdy drugi parametr ma lepszą sekwencję konwersji dla#3
(parametr, któryconst
musi przejść dodatkowąconst
kwalifikację, więc ma gorszą sekwencję konwersji). Tenconst
flip-flop powoduje, że nie jesteśmy w stanie preferować żadnego z nich.W rezultacie cała rozdzielczość przeciążenia jest niejednoznaczna.
To nie jest poprawne Bezwarunkowo rozważamy przepisanie i zmianę kandydatów. Zasada jest taka, że z [over.match.oper] / 9 :
Oznacza to, że nadal rozważamy tych kandydatów. Ale jeśli najlepszym opłacalnym kandydatem jest taki,
operator==
który zwraca, powiedzmyMeta
- 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,
Meta
jest nieistotny - problem istniałby również, gdyby został zwróconybool
.Na szczęście poprawka tutaj jest łatwa:
Po utworzeniu obu operatorów porównania
const
nie ma już dwuznaczności. Wszystkie parametry są takie same, więc wszystkie sekwencje konwersji są banalnie takie same.#1
pokonałby teraz#3
nie przez przepisanie i pokonałby#2
teraz, że nie zostałby#3
cofnięty - co czyni#1
najlepszego możliwego kandydata. Ten sam wynik, który mieliśmy w C ++ 17, tylko kilka kroków, aby się tam dostać.źródło
==
a typ powrotu wybranej funkcji nie jestbool
. Ale to ubijanie nie występuje podczas rozwiązywania problemu przeciążenia.cv bool
(i przed tą zmianą wymagano konwersji kontekstowej nabool
- wciąż nie!
)[over.match.best] / 2 pokazuje, w jaki sposób priorytetowe są prawidłowe przeciążenia w zestawie. Sekcja 2.8 mówi nam, że
F1
jest lepiej niżF2
gdyby (wśród wielu innych rzeczy):Przykład pokazuje wyraźne
operator<
wywołanie, mimo żeoperator<=>
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ć
const
funkcje. A ich brakconst
powoduje, że w grę wchodzą wcześniejsze aspekty rozwiązania problemu przeciążenia. Żadna funkcja jest dokładne dopasowanie, jak nie-const
-to-const
konwersje 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.
źródło
const
nieodwróconych kandydatów ma lepszą sekwencję konwersji dla drugiego argumentu, a odwrócony kandydat ma lepszą sekwencję konwersji dla pierwszego argumentu.const
w minimalnym przykładzie. Jestem pewien, że Eigen używaconst
wszędzie (lub poza definicjami klas, także zconst
referencjami), ale muszę to sprawdzić. Kiedy znajdę czas, staram się rozbić ogólny mechanizm wykorzystywany przez Eigen na minimalny przykład.Mamy podobne problemy z naszymi plikami nagłówkowymi Goopax. Kompilowanie poniższych z clang-10 i -std = c ++ 2a powoduje błąd kompilatora.
Zapewnienie tych dodatkowych operatorów wydaje się rozwiązać problem:
źródło
a == 0
się skompilował ?gpu_bool gpu_type<T>::operator==(T a) const;
agpu_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 jestbool
.