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;
 }
Ahmed Nawar
źródło
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==.

Tom Tanner
źródło
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.

Lukáš Bednařík
źródło
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.

Christian Hackl
źródło
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.

Richarda Hodgesa
źródło
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!=).


źródło
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.

Margus
źródło