Dlaczego nierówność jest testowana jako (! (A == b)) w wielu standardowych kodach bibliotek C ++?


142

To jest kod ze standardowego removekodu biblioteki C ++ . Dlaczego nierówność jest testowana jako if (!(*first == val))zamiast if (*first != val)?

 template <class ForwardIterator, class T>
      ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val)
 {
     ForwardIterator result = first;
     while (first!=last) {
         if (!(*first == val)) {
             *result = *first;
             ++result;
         }
         ++first;
     }
     return result;
 }

2
@BeyelerStudios jest prawdopodobnie poprawne. Jest to również powszechne podczas wdrażania operator!=. Po prostu skorzystaj z operator==implementacji:bool operator!=(const Foo& other) { return !(*this == other); }
— simon

1
właściwie poprawiam moje stwierdzenie: wzmianka o referencjach usuwa wszystkie elementy, które są równe wartości, więc operator==oczekuje się, że zostanie tutaj użyty ...
— BeyelerStudios

Aha, iw constprzykładzie w moim poprzednim komentarzu powinno być również , ale o co chodzi. (Za późno, aby to edytować)
— simon,

Przyczyna tego jest związana z innym pytaniem (na które można zasadniczo odpowiedzieć „Nie, niekoniecznie”) oraz pojęciem bycia, o EqualityComparablektórym Hurkyl wspomniał w swojej odpowiedzi .
— Marco13

Odpowiedzi:


144

Ponieważ oznacza to, że jedynym wymaganiem na T jest implementacja operator==. Możesz wymagać od T, aby mieć, operator!=ale ogólna idea jest taka, że ​​powinieneś obciążać użytkownika szablonu jak najmniejszym, a inne szablony wymagają operator==.


13
szablon <klasa T> inline bool operator! = <T a, T b> {return! (a == b); }
— Joshua

8
Czy byłby scenariusz, w którym kompilator nie byłby w stanie zamienić wszystkich wystąpień =! do! (==)? Dlaczego nie miałoby to być już domyślną czynnością wykonywaną przez kompilator?
— Aidan Gomez,

20
@AidanGomez Na dobre lub na złe możesz przeciążać operatorów, aby robili, co chcesz. To nie musi mieć sensu ani być konsekwentne.
— Neil Kirk

10
x != ynie jest zdefiniowany jako taki sam jak !(x == y). Co się stanie, jeśli te operatory zwrócą drzewo analizy osadzonego DSL?
— Brice M. Dempsey

7
@Joshua To źle się psuje, jeśli próbujesz użyć SFINAE do wykrycia, czy !=jest obsługiwana (niepoprawnie zwróci wartość true - nawet jeśli operator==nie jest obsługiwana!). Martwię się również, że spowoduje to !=niejednoznaczność niektórych zastosowań .

36

Większość funkcji w STL działa tylko z operator<lub operator==. Wymaga to od użytkownika tylko zaimplementowania tych dwóch operatorów (lub czasami przynajmniej jednego z nich). Na przykład std::setużywa operator<(a dokładniej, std::lessktóra wywołuje operator<domyślnie), a nie operator>do zarządzania kolejnością. removeSzablon w przykładzie jest podobny przypadek - używa tylko operator==i nie operator!=tak operator!=nie musi być zdefiniowana.


2
Funkcje nie używają operator<bezpośrednio, ale zamiast tego używają std::less, co z kolei domyślnie operator<.
— Christian Hackl,

1
Właściwie wydaje się, że standardowe funkcje algorytmów, w przeciwieństwie do np. std::set, Rzeczywiście używają operator<bezpośrednio. Dziwne ...
— Christian Hackl

1
Te funkcje nie używają std::equal_to, używają operator==jak wskazano w pytaniu. Sytuacja std::lessjest podobna. Cóż, może std::setnie jest to najlepszy przykład.
— Lukáš Bednařík

2
@ChristianHackl std::equal_toi std::lesssą używane jako domyślne parametry szablonu, w których jako parametr przyjmuje się komparator. operator==i operator<są używane bezpośrednio tam, gdzie typ jest wymagany do spełnienia równości, odpowiednio, porównywalnej i ścisłej słabej kolejności, np. iteratory i iteratory o swobodnym dostępie.
— Jan Hudec

28

To jest kod z kodu usuwania biblioteki standardowej C ++.

Źle. To nie jest standardowy removekod biblioteki C ++ . Jest to jedna z możliwych implementacji wewnętrznej biblioteki standardowej C ++remove funkcji . Standard C ++ nie określa faktycznego kodu; zawiera prototypy funkcji i wymagane zachowania.

Innymi słowy: ze ścisłego punktu widzenia języka kod, który widzisz , nie istnieje . Może pochodzić z jakiegoś pliku nagłówkowego, który jest dostarczany z implementacją biblioteki standardów kompilatora. Zauważ, że standard C ++ nie wymaga nawet istnienia tych plików nagłówkowych . Pliki są po prostu wygodnym sposobem dla implementujących kompilatory, aby spełnić wymagania dla linii #include <algorithm>(tj. std::removeUdostępnianie i inne funkcje).

Dlaczego nierówność jest testowana jako if (!(*first == val))zamiast if (*first != val)?

Ponieważ operator==wymaga tego tylko funkcja.

Jeśli chodzi o przeciążanie operatorów dla typów niestandardowych, język pozwala robić różne dziwne rzeczy. Możesz bardzo dobrze stworzyć klasę, która ma przeciążoną, operator==ale nie przeciążoną operator!=. Lub nawet gorzej: możesz przeciążaćoperator!= ale sprawić, by robił zupełnie niezwiązane rzeczy.

Rozważmy ten przykład:

#include <algorithm>
#include <vector>

struct Example
{
    int i;

    Example() : i(0) {}

    bool operator==(Example const& other) const
    {
        return i == other.i;
    }

    bool operator!=(Example const& other) const
    {
        return i == 5; // weird, but nothing stops you
                       // from doing so
    }

};

int main()
{
  std::vector<Example> v(10);
  // ...
  auto it = std::remove(v.begin(), v.end(), Example());
  // ...
}

Jeśli zostanie std::removeużyty operator!=, wynik byłby zupełnie inny.


1
Inną rzeczą do rozważenia jest to, że może być możliwe dla obu a==bi a!=bzwrócenia fałszu. Chociaż nie zawsze może być jasne, czy taka sytuacja byłaby bardziej sensownie traktowana jako „równa” czy „nierówna”, funkcja, która definiuje równość wyłącznie na podstawie operatora „==”, musi traktować je jako „nierówne” ", bez względu na to, które zachowanie miałoby więcej sensu [gdybym miał swoje wybory, od wszystkich typów należałoby oczekiwać, że operatory logiczne" == "i"! = "będą działały konsekwentnie, ale fakt, że IEEE-754 nakazuje złamanie równości operatorzy utrudnialiby takie oczekiwanie].
— supercat

18
„Ze ścisłego punktu widzenia języka kod, który widzisz, nie istnieje”. - kiedy punkt widzenia mówi, że coś nie istnieje, ale tak naprawdę patrzysz na to, to punkt widzenia jest zły. W rzeczywistości standard nie mówi, że kod nie istnieje, po prostu nie mówi, że istnieje :-)
— Steve Jessop

@SteveJessop: Możesz zastąpić wyrażenie „ze ścisłego językowego punktu widzenia” czymś w rodzaju „ściśle, na poziomie języka”. Chodzi o to, że kod opublikowany przez PO nie dotyczy standardu językowego.
— Christian Hackl

2
@supercat: IEEE-754 tworzy ==i !=zachowuje się konsekwentnie, chociaż zawsze uważałem, że wszystkie sześć relacji powinno się oceniać, falsegdy co najmniej jeden operand jest NaN.
— Ben Voigt,

@BenVoigt: Ach, zgadza się. To sprawia, że ​​dwa operatory zachowują się w ten sam zepsuty sposób, tak że są ze sobą spójne, ale nadal udaje im się naruszyć wszystkie inne normalne aksjomaty związane z równoważnością (np. Nie podtrzymują ani a == a, ani gwarancji, że operacje zostały wykonane na równych wartościach da równe wyniki).
— supercat

15

Kilka dobrych odpowiedzi. Chciałem tylko dodać małą notatkę.

Podobnie jak wszystkie dobre biblioteki, biblioteka standardowa została zaprojektowana z uwzględnieniem (co najmniej) dwóch bardzo ważnych zasad:

  1. Połóż najmniejszą odpowiedzialność na użytkownikach swojej biblioteki, z którą możesz uciec. Częściowo wiąże się to z poświęceniem im najmniejszej ilości pracy podczas korzystania z interfejsu. (np. zdefiniowanie jak najmniejszej liczby operatorów). Druga część polega na tym, że nie zaskakuje ich ani nie wymaga od nich sprawdzania kodów błędów (więc zachowaj spójność interfejsów i zgłaszaj wyjątki, <stdexcept>gdy coś pójdzie nie tak).

  2. Wyeliminuj całą logiczną nadmiarowość . Wszystkie porównania można wyprowadzić po prostu zoperator< , więc po co żądać, aby użytkownicy definiowali innych? na przykład:

    (a> b) jest równoważne (b <a)

    (a> = b) jest równoważne! (a <b)

    (a == b) jest równoważne! ((a <b) || (b <a))

    i tak dalej.

    Oczywiście w tej notatce można zapytać, dlaczego unordered_mapwymaga operator==(przynajmniej domyślnie), a nie operator<. Odpowiedź jest taka, że ​​w tabeli skrótów jedyne porównanie, jakiego kiedykolwiek potrzebujemy, dotyczy równości. Dlatego jest bardziej logicznie spójne (tj. Ma większy sens dla użytkownika biblioteki), aby wymagać od niego zdefiniowania operatora równości. Wymaganie operator<byłoby mylące, ponieważ nie jest od razu oczywiste, dlaczego go potrzebujesz.


10
Odnośnie drugiego punktu: istnieją typy, które można logicznie porównać pod kątem równości, nawet jeśli nie istnieje żaden logiczny porządek, lub dla których ustalenie porządku byłoby bardzo sztuczne. Na przykład czerwony jest czerwony, a czerwony nie jest zielony, ale czy czerwony jest z natury mniejszy od zielonego?
— Christian Hackl,

1
Zgadzać się w zupełności. Nie można przechowywać tych pozycji w zamówionym kontenerze, ponieważ nie ma logicznej kolejności. Mogą być bardziej odpowiednio przechowywane w nieuporządkowanym pojemniku, który wymaga operator==(i hash).
— Richard Hodges

3. Przeciąż najmniej standardowe operatory, jak to możliwe. To kolejny powód, dla którego wdrożyli !(a==b). Ponieważ niezrefleksowane przeciążenie operatorów może łatwo spowodować całkowite zepsucie programu C ++ (plus, sprawić, że programista oszaleje, ponieważ debugowanie jego kodu może stać się misją niemożliwą, ponieważ znalezienie sprawcy konkretnego błędu przypomina odyseję).
— błąd składni

!((a < b) || (b < a))używa jednego operatora bool mniej, więc prawdopodobnie jest szybszy
— Filip Haglund

1
W rzeczywistości tak nie jest. W języku asemblera wszystkie porównania są implementowane jako odejmowanie, po którym następuje testowanie przeniesienia i zero bitów w rejestrze flag. Wszystko inne to tylko cukier syntaktyczny.
— Richard Hodges

8

EqualityComparablePojęcie tylko wymaga,operator== być zdefiniowane.

W konsekwencji żadna funkcja, która twierdzi, że działa z typami spełniającymi, EqualityComparable nie może polegać na istnieniu operator!=obiektów for tego typu. (chyba że istnieją dodatkowe wymagania, które implikują istnienie operator!=).


1

Najbardziej obiecującym podejściem jest znalezienie metody określania, czy operator == można wywołać dla określonego typu, a następnie wspieranie go tylko wtedy, gdy jest dostępny; w innych sytuacjach zostałby zgłoszony wyjątek. Jednak do tej pory nie jest znany sposób wykrycia, czy dowolne wyrażenie operatora f == g jest odpowiednio zdefiniowane. Najlepsze znane rozwiązanie ma następujące niepożądane cechy:

  • Niepowodzenie w czasie kompilacji dla obiektów, w których operator == nie jest dostępny (np. Ponieważ jest prywatny).
  • Niepowodzenie w czasie kompilacji, jeśli operator wywołujący == jest niejednoznaczny.
  • Wydaje się poprawne, jeśli deklaracja operatora == jest poprawna, nawet jeśli operator == może się nie kompilować.

Od Często zadawane pytania dotyczące Boost: źródło

Wiedząc, że wymaganie ==implementacji jest obciążeniem , nigdy nie chcesz stwarzać dodatkowego obciążenia wymaganiem!= implementacji.

Dla mnie osobiście chodzi o SOLID (projektowanie obiektowe) L część - zasada podstawienia Liskova: „obiekty w programie powinny być zastępowalne instancjami ich podtypów bez zmiany poprawności tego programu”. W tym przypadku jest to operator ! = , Który mogę zastąpić znakiem == i odwrotnością boolowską w logice boolowskiej.

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.