Historia pytania
Przed C++20 deweloperzy ręcznie implementowali sześć operatorów porównawczych dla typów, które można sortować. Ten szablon często wprowadzał subtelne niespójności logiczne między równościami a relacjami porządkującymi. Operator statku kosmicznego został wprowadzony, aby skonsolidować te operatorzy w jedną kanoniczną operację.
Problem
Chociaż operator<=> zmniejsza składnię, kompilator polega na jego typie zwracanym, aby syntetyzować odwrotne wyrażenia, takie jak b < a z a > b. Bez wiedzy na temat tego, czy porządek jest silny, słaby czy częściowy, kompilator nie może bezpiecznie generować tych przekształceń.
Rozwiązanie
Typ zwracany musi być std::strong_ordering, std::weak_ordering lub std::partial_ordering (lub niejawnie konwertowalny). Ta standardowa kategoria umożliwia kompilatorowi generowanie odwrotnych kandydatów i niejawnych sprawdzeń równości. Zwracanie auto lub typów niestandardowych wyłącza tę syntezę, wymagając ręcznych przeciążeń asymetrycznych.
struct Widget { int id; // Poprawnie: umożliwia generowanie odwrotnych kandydatów std::strong_ordering operator<=>(const Widget&) const = default; };
Scenariusz i problem
Opracowanie SpatialIndex do akcelerowanej przez GPU geometrii wymagało struktury BoundingBox z surowym słabym porządkiem do wstawiania do std::set. Pudła musiały być porównywane z surowymi tablicami współrzędnych w zapytaniach przestrzennych.
Rozwiązanie 1: Ręczne przeciążanie operatorów
Implementacja dwunastu przeciążeń (sześć dla BoundingBox, sześć dla tablic współrzędnych) zapewniła wyraźną kontrolę. Jednak ta obfitość narażała na błędy kopiuj-wklej między operator< a operator>, a utrzymanie spójności podczas refaktoryzacji okazało się żmudne.
Rozwiązanie 2: Domyślny statek kosmiczny zwracający std::weak_ordering
To automatycznie generowało wszystkie operatory relacyjne z jednej deklaracji. Wyraźny typ zwracany pozwolił kompilatorowi obsłużyć odwrotne porównania z tablicami współrzędnych. Implementacja zagwarantowała bezpieczeństwo wyjątków i matematyczną spójność bez szablonów.
Rozwiązanie 3: Zwracanie auto
Użycie auto operator<=>(const BoundingBox&) const = default uniemożliwiło syntezę odwrotnych kandydatów. Porównanie surowej tablicy z lewej strony z BoundingBox z prawej strony nie kompilowało się. Ta asymetria zepsuła interfejs zapytania przestrzennego.
Decyzja i wynik
Wybraliśmy Rozwiązanie 2 z std::weak_ordering, ponieważ prostokąty ograniczające mają równoważność (przecinające się prostokąty porównują się na równi), ale nie mają równości matematycznej. To umożliwiło płynne zintegrowanie z standardowymi algorytmami przy jednoczesnym wspieraniu heterogenicznych porównań współrzędnych.
Dlaczego kompilator syntetyzuje operator== z operator<=>, i kiedy jest to suboptymalne?
Kompilator generuje operator== jako ((*this <=> other) == 0). To zapewnia spójność, ale zmusza do pełnego porównania elementów, nawet przy sprawdzaniu równości. Wyraźne domyślne ustawienie operator== pozwala na oceny krótkiego kursu, natychmiast zwracając false po pierwszym różniącym się członku.
Jak zdefiniowanie operator<=> jako członka, a nie jako ukrytego przyjaciela, zaburza symetrię?
Członek operator<=> zezwala tylko na niejawne konwersje na prawym operandsie podczas rozwiązywania przeciążeń. Ta asymetria uniemożliwia kompilację wyrażeń takich jak double == MyClass, nawet jeśli MyClass można skonstruować z double. Użycie ukrytego przyjaciela umożliwia Argument Dependent Lookup (ADL), co pozwala na niejawne konwersje obu operandów.
Co odróżnia std::compare_three_way od ręcznego porównania wskaźników?
std::compare_three_way zapewnia całkowity porządek dla wskaźników, który jest spójny w całej przestrzeni adresowej, w tym std::nullptr_t. Ręczne porównania wskaźników przy użyciu operatorów relacyjnych wywołują niezdefiniowane zachowanie podczas porównywania niepowiązanych obiektów. Użycie standardowego obiektu funkcyjnego zapewnia przenośną, dobrze zdefiniowaną semantykę dla sortowania wskaźników.