Czy == i! = Są od siebie zależne?

292

Uczę się o przeciążenia operatora w C ++, i widzę, że ==i !=są po prostu pewne specjalne funkcje, które mogą być dostosowane do typów zdefiniowanych przez użytkownika. Martwię się jednak, dlaczego potrzebne są dwie osobne definicje? Myślałem, że jeśli a == bto prawda, a != bto automatycznie jest fałszem i odwrotnie, i nie ma innej możliwości, ponieważ z definicji tak a != bjest !(a == b). I nie wyobrażałem sobie żadnej sytuacji, w której to nie byłoby prawdą. Ale może moja wyobraźnia jest ograniczona, czy czegoś nie wiem?

Wiem, że mogę zdefiniować jedno w kategoriach drugiego, ale nie o to pytam. Nie pytam też o rozróżnienie między porównywaniem obiektów według wartości lub tożsamości. Lub czy dwa obiekty mogą być równe i nierównomierne jednocześnie (to zdecydowanie nie jest opcja! Te rzeczy wzajemnie się wykluczają). Pytam o to:

Czy jest jakakolwiek sytuacja, w której zadawanie pytań o równość dwóch przedmiotów ma sens, ale pytanie o nierówność między nimi nie ma sensu? (z perspektywy użytkownika lub z perspektywy implementatora)

Jeśli nie ma takiej możliwości, to dlaczego na Ziemi C ++ mają te dwa operatory zdefiniowane jako dwie odrębne funkcje?

BarbaraKwarc
źródło
13
Dwa wskaźniki mogą być zerowe, ale niekoniecznie równe.
Ali Caglayan
2
Nie jestem pewien, czy ma to sens, ale po przeczytaniu tego pomyślałem o problemach zwarciowych. Na przykład można zdefiniować, że 'undefined' != expressionzawsze jest to prawda (lub fałsz, lub niezdefiniowane), niezależnie od tego, czy wyrażenie może być ocenione. W takim przypadku a!=bzwróci prawidłowy wynik zgodnie z definicją, ale !(a==b)nie powiedzie się, jeśli nie będzie bmożna go ocenić. (Lub poświęć dużo czasu, jeśli ocena bjest droga).
Dennis Jaheruddin
2
Co z null! = Null and null == null? Może to być zarówno ... więc jeśli a! = B to nie zawsze oznacza a == b.
zozo
4
Przykład z javascript(NaN != NaN) == true
chiliNUT

Odpowiedzi:

272

Byś nie chcą językiem automatycznie przepisać a != bjako !(a == b)kiedy a == bpowróci coś innego niż bool. Istnieje kilka powodów, dla których możesz to zrobić.

Możesz mieć obiekty konstruktora wyrażeń, w których a == bnie ma i nie ma na celu przeprowadzania żadnego porównania, ale po prostu buduje jakiś reprezentujący węzeł wyrażenia a == b.

Możesz mieć leniwą ocenę, w której a == bnie ma i nie ma na celu bezpośredniego porównania, ale zamiast tego zwraca pewien rodzaj tego, lazy<bool>który można później przekształcić w boolsposób niejawny lub jawny, aby faktycznie wykonać porównanie. Prawdopodobnie w połączeniu z obiektami konstruktora wyrażeń, aby umożliwić pełną optymalizację wyrażeń przed oceną.

Możesz mieć niestandardową optional<T>klasę szablonów, w której podane są opcjonalne zmienne ti uchcesz zezwolić t == u, ale spraw , aby powróciła optional<bool>.

Jest chyba coś więcej, o czym nie myślałem. I chociaż w tych przykładach operacja a == bi a != boba mają sens, nadal a != bnie jest tym samym !(a == b), więc potrzebne są osobne definicje.


źródło
72
Budowanie wyrażeń to fantastyczny praktyczny przykład tego, kiedy chcesz tego, co nie zależy od wymyślonych scenariuszy.
Oliver Charlesworth,
6
Innym dobrym przykładem są wektorowe operacje logiczne. Wolisz jedno przejście przez dane obliczeniowej !=zamiast dwóch przejściach obliczeniowych ==potem !. Zwłaszcza w czasach, w których nie można było polegać na kompilatorze łączącym pętle. Lub nawet dzisiaj, jeśli nie uda ci się przekonać kompilatora, twoje wektory się nie pokrywają.
41
„Możesz mieć wyraz budowniczy obiektów” - no więc operator !może także zbudować jakiś węzeł ekspresji i jesteśmy nadal w porządku wymianie a != bz !(a == b)tak dalece, jak to idzie. To samo dotyczy lazy<bool>::operator!, może wrócić lazy<bool>. optional<bool>jest bardziej przekonujący, ponieważ logiczna prawdomówność boost::optionalzależy na przykład od tego, czy wartość istnieje, a nie od samej wartości.
Steve Jessop,
42
Wszystko to i Nans - proszę pamiętać o NaNs;
jsbueno
9
@ jsbueno: Zwrócono uwagę, że NaN nie są pod tym względem wyjątkowi.
Oliver Charlesworth,
110

Jeśli nie ma takiej możliwości, to dlaczego na Ziemi C ++ mają te dwa operatory zdefiniowane jako dwie odrębne funkcje?

Ponieważ możesz je przeciążyć, a przeciążając je, możesz nadać im zupełnie inne znaczenie niż ich oryginalne.

Weźmy na przykład operator <<, pierwotnie bitowy operator przesunięcia w lewo, obecnie często przeciążony jako operator wstawiania, jak w std::cout << something; zupełnie inne znaczenie niż oryginalne.

Jeśli więc zaakceptujesz, że znaczenie operatora zmienia się, gdy go przeciążasz, nie ma powodu, aby uniemożliwić użytkownikowi nadanie operatorowi znaczenia, ==które nie jest dokładnie zaprzeczeniem operatora !=, choć może to być mylące.

dzierzba
źródło
18
To jedyna odpowiedź, która ma sens praktyczny.
Sonic Atom
2
Wydaje mi się, że masz przyczynę i skutek wstecz. Można je oddzielnie, ponieważ przeciążenie ==i !=istnieć jako odrębne podmioty. Z drugiej strony prawdopodobnie nie istnieją one jako odrębne operatory, ponieważ można je przeciążać osobno, ale ze względu na starszą i wygodę (zwięzłość kodu).
nitro2k01
60

Martwię się jednak, dlaczego potrzebne są dwie osobne definicje?

Nie musisz definiować obu.
Jeśli wzajemnie się wykluczają, nadal możesz być zwięzły, definiując tylko ==i <obok std :: rel_ops

Z preferencji:

#include <iostream>
#include <utility>

struct Foo {
    int n;
};

bool operator==(const Foo& lhs, const Foo& rhs)
{
    return lhs.n == rhs.n;
}

bool operator<(const Foo& lhs, const Foo& rhs)
{
    return lhs.n < rhs.n;
}

int main()
{
    Foo f1 = {1};
    Foo f2 = {2};
    using namespace std::rel_ops;

    //all work as you would expect
    std::cout << "not equal:     : " << (f1 != f2) << '\n';
    std::cout << "greater:       : " << (f1 > f2) << '\n';
    std::cout << "less equal:    : " << (f1 <= f2) << '\n';
    std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}

Czy jest jakakolwiek sytuacja, w której zadawanie pytań o równość dwóch przedmiotów ma sens, ale pytanie o nierówność między nimi nie ma sensu?

Często kojarzymy tych operatorów z równością.
Chociaż tak zachowują się na podstawowych typach, nie ma obowiązku, aby takie było ich zachowanie na niestandardowych typach danych. Nie musisz nawet zwracać bool, jeśli nie chcesz.

Widziałem ludzi przeciążających operatorów w dziwny sposób, ale okazało się, że ma to sens w przypadku ich aplikacji. Nawet jeśli interfejs wydaje się wskazywać, że wzajemnie się wykluczają, autor może chcieć dodać określoną logikę wewnętrzną.

(z perspektywy użytkownika lub z perspektywy implementatora)

Wiem, że chcesz konkretnego przykładu,
więc oto jeden z frameworka Catch testing, który moim zdaniem był praktyczny:

template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
    return captureExpression<Internal::IsEqualTo>( rhs );
}

template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
    return captureExpression<Internal::IsNotEqualTo>( rhs );
}

Operatorzy ci robią różne rzeczy i nie ma sensu definiować jednej metody jako! (Nie) drugiej. Powodem tego jest to, że środowisko może wydrukować wykonane porównanie. W tym celu musi uchwycić kontekst użytego przeciążonego operatora.

Trevor Hickey
źródło
14
O rany, jak mogłem nie wiedzieć std::rel_ops? Dziękuję bardzo za zwrócenie na to uwagi.
Daniel Jour
5
Prawie dosłowne kopie z cppreferencji (lub gdziekolwiek indziej) powinny być wyraźnie oznaczone i właściwie przypisane. rel_opsi tak jest okropny.
TC
@TC Zgoda, mówię tylko, że jest to metoda, którą OP może przyjąć. Nie wiem, jak wyjaśnić rel_ops w prostszy sposób niż pokazany przykład. Podałem link do tego, gdzie to jest, ale opublikowałem kod, ponieważ strona referencyjna zawsze mogła się zmienić.
Trevor Hickey,
4
Nadal musisz wyjaśnić, że przykład kodu pochodzi w 99% z preferencji cp, a nie z twojego.
TC
2
Wygląda na to, że Std :: relops popadło w niełaskę. Sprawdź opcje wsparcia dla czegoś bardziej ukierunkowanego.
JDługosz
43

Istnieje kilka bardzo dobrze ustalone konwencje, które (a == b)i (a != b)zarówno fałszywe niekoniecznie przeciwieństwa. W szczególności w SQL każde porównanie z NULL daje NULL, nie jest prawdą ani fałszem.

Prawdopodobnie nie jest dobrym pomysłem tworzenie nowych przykładów, jeśli to w ogóle możliwe, ponieważ jest to tak mało intuicyjne, ale jeśli próbujesz modelować istniejącą konwencję, fajnie jest mieć opcję, aby Twoi operatorzy zachowywali się „poprawnie” w tym celu kontekst.

Jander
źródło
4
Wdrażasz zachowanie zerowe podobne do SQL w C ++? Ewwww. Ale przypuszczam, że nie jest to coś, co moim zdaniem powinno być zakazane w języku, jakkolwiek by to było niesmaczne.
1
@ dan1111 Co ważniejsze, niektóre smaki SQL mogą być dobrze napisane w c ++, więc język musi obsługiwać ich składnię, prawda?
Joe
1
Popraw mnie, jeśli się mylę, po prostu wychodzę z wikipedii , ale czy porównanie z wartością NULL w SQL zwraca Nieznany, czy fałszywy? I czy negacja Nieznanego wciąż nie jest znana? Więc jeśli logika SQL została zakodowana w C ++, nie chciałbyś NULL == somethingzwrócić Nieznanego, a także chciałbyś NULL != somethingzwrócić Nieznany i chciałbyś !Unknownzwrócić Unknown. I w takim przypadku implementacja operator!=jako negacja operator==jest nadal poprawna.
Benjamin Lindley
1
@Barmar: OK, ale w jaki sposób poprawia to stwierdzenie „SQL NULL działa w ten sposób” ? Jeśli ograniczamy nasze implementacje operatora porównania do zwracających wartości logiczne, to nie oznacza to tylko, że implementacja logiki SQL z tymi operatorami jest niemożliwa?
Benjamin Lindley,
2
@Barmar: Cóż, nie o to chodzi. PO już o tym wie, bo inaczej to pytanie nie istniałoby. Chodziło o to, aby przedstawić przykład, w którym miało sens albo 1) zaimplementowanie jednego, operator==albo operator!=nie drugiego, albo 2) wdrożenie operator!=w sposób inny niż negacja operator==. A implementacja logiki SQL dla wartości NULL nie jest tego przypadkiem.
Benjamin Lindley,
23

Odpowiem tylko na drugą część twojego pytania, a mianowicie:

Jeśli nie ma takiej możliwości, to dlaczego na Ziemi C ++ mają te dwa operatory zdefiniowane jako dwie odrębne funkcje?

Jednym z powodów, dla których warto pozwolić programistom na przeciążenie obu, jest wydajność. Możesz zezwolić na optymalizacje, wdrażając zarówno ==i !=. Wtedy x != ymoże być tańszy niż !(x == y)jest. Niektóre kompilatory mogą być w stanie zoptymalizować go dla Ciebie, ale być może nie, szczególnie jeśli masz złożone obiekty z dużą ilością rozgałęzień.

Nawet w Haskell, gdzie programiści bardzo poważnie podchodzą do praw i pojęć matematycznych, nadal można przeciążać oba == i/= , jak widać tutaj ( http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude .html # v: -61--61- ):

$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
λ> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
        -- Defined in `GHC.Classes'

Prawdopodobnie byłoby to uważane za mikrooptymalizację, ale w niektórych przypadkach może być uzasadnione.

Centril
źródło
3
Świetnym przykładem tego są klasy opakowań SSE (x86 SIMD). Jest pcmpeqbinstrukcja, ale nie ma instrukcji porównawczej, która tworzy maskę! =. Więc jeśli nie możesz po prostu odwrócić logiki tego, co korzysta z wyników, musisz użyć innej instrukcji, aby ją odwrócić. (Ciekawostka: zestaw instrukcji XOP AMD ma już wiele zestawień neq. Szkoda, że ​​Intel nie przyjął / nie rozszerzył XOP; w przydatnym rozszerzeniu ISA znajduje się kilka przydatnych instrukcji.)
Peter Cordes,
1
Najważniejszym aspektem SIMD jest przede wszystkim wydajność, a zazwyczaj z trudem używasz go ręcznie w pętlach ważnych dla ogólnej wydajności. Zapisanie pojedynczej instrukcji ( PXORrazem ze wszystkimi, aby odwrócić wynik porównania maski) w ciasnej pętli może mieć znaczenie.
Peter Cordes,
Wydajność jako przyczyna nie jest wiarygodna, gdy narzut stanowi jedną logiczną negację .
Pozdrawiam i hth. - Alf
Może to być więcej niż jedna logiczna negacja, jeśli obliczenia x == ykosztują znacznie więcej niż x != y. Obliczenie tego drugiego może być znacznie tańsze ze względu na przewidywanie gałęzi itp.
Centril
16

Czy jest jakakolwiek sytuacja, w której zadawanie pytań o równość dwóch przedmiotów ma sens, ale pytanie o nierówność między nimi nie ma sensu? (z perspektywy użytkownika lub z perspektywy implementatora)

To jest opinia. Może nie. Ale projektanci języka, nie będąc wszechwiedzący, postanowili nie ograniczać ludzi, którzy mogą wymyślić sytuacje, w których może to mieć sens (przynajmniej dla nich).

Benjamin Lindley
źródło
13

W odpowiedzi na edycję;

To znaczy, jeśli możliwe jest, aby jakiś typ miał operatora, ==ale nie !=odwrotnie, i kiedy ma to sens.

W ogóle , nie, to nie ma sensu. Operatory równości i relacji są zazwyczaj dostępne w zestawach. Jeśli jest równość, to również nierówność; mniej niż, to więcej niż i tak dalej z<= itd. Podobne podejście stosuje się również do operatorów arytmetycznych, one również ogólnie występują w naturalnych zestawach logicznych.

Dowodzi tego fakt std::rel_ops przestrzeń nazw. Jeśli zaimplementujesz równość i mniej niż operatorów, użycie tej przestrzeni nazw daje ci inne, zaimplementowane w kategoriach oryginalnych zaimplementowanych operatorów.

To powiedziawszy, czy istnieją warunki lub sytuacje, w których jedno nie oznaczałoby od razu drugiego lub nie można go wdrożyć w odniesieniu do innych? Tak, jest ich zapewne niewiele, ale oni tam są; ponownie, o czym świadczy rel_opsistnienie własnej przestrzeni nazw. Z tego powodu umożliwienie ich niezależnej implementacji umożliwia wykorzystanie języka w celu uzyskania potrzebnej lub potrzebnej semantyki w sposób, który jest nadal naturalny i intuicyjny dla użytkownika lub klienta kodu.

Wspomniana już leniwa ocena jest tego doskonałym przykładem. Innym dobrym przykładem jest nadanie im semantyki, która wcale nie oznacza równości lub nierówności. Podobnym przykładem są operatory przesunięcia bitów <<i >>stosowane do wstawiania i wydobywania strumienia. Chociaż może się to spotkać z niechęcią w kręgach ogólnych, w niektórych obszarach specyficznych dla domeny może mieć sens.

Niall
źródło
12

Jeśli ==i !=operatorzy nie faktycznie oznacza, równość, w taki sam sposób, że <<i>> operatorzy stream nie oznaczają przesunięcie bitów. Jeśli traktujesz te symbole tak, jakby oznaczały jakąś inną koncepcję, nie muszą się wzajemnie wykluczać.

Jeśli chodzi o równość, może mieć sens, jeśli twoja przypadek użycia uzasadnia traktowanie obiektów jako nieporównywalnych, aby każde porównanie zwróciło fałsz (lub nieporównywalny typ wyniku, jeśli operatorzy zwrócą wartość inną niż bool). Nie mogę wymyślić konkretnej sytuacji, w której byłoby to uzasadnione, ale widziałem, że jest to wystarczająco rozsądne.

Taywee
źródło
7

Z wielką mocą przychodzi świetnie odpowiedzialny, a przynajmniej naprawdę dobry styl przewodników.

==i !=może być przeciążony, aby zrobić cokolwiek chcesz. To zarówno błogosławieństwo, jak i przekleństwo. Nie ma gwarancji, że !=to oznacza !(a==b).

It's Pet
źródło
6
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);

Nie mogę uzasadnić przeciążenia tego operatora, ale w powyższym przykładzie nie można zdefiniować operator!=jako „przeciwieństwo” operator==.

Dafang Cao
źródło
1
@Snowman: Dafang nie twierdzi, że jest to dobre wyliczenie (ani dobry pomysł na zdefiniowanie takiego wyliczenia), to tylko przykład ilustrujący jakiś punkt. Przy tej (być może złej) definicji operatora !=tak naprawdę nie oznaczałoby to odwrotności ==.
AlainD
1
@AlainD czy kliknąłeś link, który zamieściłem i czy znasz cel tej witryny? Nazywa się to „humorem”.
1
@Snowman: Z pewnością ... przepraszam, brakowało mi tego linku i było ironiczne! : o)
AlainD
Zaraz, przeciążasz jednoargumentowo ==?
LF,
5

W końcu sprawdzasz za pomocą tych operatorów, czy wyrażenie a == blub a != bzwraca wartość logiczną ( truelub false). Wyrażenie to zwraca wartość logiczną po porównaniu, a nie wyklucza się wzajemnie.

Anirudh Sohil
źródło
4

[..] dlaczego potrzebne są dwie oddzielne definicje?

Jedną rzeczą do rozważenia jest to, że może istnieć możliwość zaimplementowania jednego z tych operatorów bardziej efektywnie niż tylko zaprzeczenie drugiemu.

(Mój przykład tutaj to śmieci, ale kwestia nadal jest ważna, pomyśl o filtrach Bloom, na przykład: Pozwalają one na szybkie testowanie, jeśli czegoś nie ma w zestawie, ale sprawdzenie, czy jest w zestawie, może zająć dużo więcej czasu.)

[..] z definicji a != bjest !(a == b).

I to jako programista ponosisz odpowiedzialność. Prawdopodobnie dobrze jest napisać test.

Daniel Jour
źródło
4
Jak !((a == rhs.a) && (b == rhs.b))nie dopuścić do zwarcia? jeśli !(a == rhs.a), to (b == rhs.b)nie będzie oceniane.
Benjamin Lindley,
To jednak zły przykład. Zwarcie nie dodaje tutaj żadnej magicznej przewagi.
Oliver Charlesworth,
@Oliver Charlesworth Alone nie robi tego, ale w połączeniu z osobnymi operatorami robi to: W przypadku ==przestanie porównywać, gdy tylko pierwsze odpowiednie elementy nie będą równe. Ale w przypadku !=, gdyby został zaimplementowany w kategoriach ==, musiałby najpierw porównać wszystkie odpowiadające mu elementy (gdy wszystkie są równe), aby móc stwierdzić, że nie są one różne: P Ale po zaimplementowaniu jak w powyższy przykład przestanie porównywać, gdy tylko znajdzie pierwszą nierównomierną parę. Rzeczywiście świetny przykład.
BarbaraKwarc,
@BenjaminLindley Prawda, mój przykład był kompletnym nonsensem. Niestety nie mogę wymyślić innego bankomatu, jest już za późno.
Daniel Jour
1
@BarbaraKwarc: !((a == b) && (c == d))i (a != b) || (c != d)są równoważne pod względem wydajności zwarciowej.
Oliver Charlesworth,
2

Dostosowując zachowanie operatorów, możesz zmusić ich do robienia tego, co chcesz.

Możesz dostosować rzeczy. Na przykład możesz dostosować klasę. Obiekty tej klasy można porównać, po prostu sprawdzając określoną właściwość. Wiedząc, że tak jest, możesz napisać określony kod, który sprawdza tylko minimalne rzeczy, zamiast sprawdzać każdy bit każdej pojedynczej właściwości w całym obiekcie.

Wyobraź sobie przypadek, w którym możesz dowiedzieć się, że coś jest inne tak samo szybko, jeśli nie szybciej, niż możesz dowiedzieć się, że coś jest takie samo. To prawda, że ​​kiedy zorientujesz się, czy coś jest takie samo czy różne, możesz poznać coś przeciwnego, po prostu trochę odwracając. Jednak odwrócenie tego bitu jest dodatkową operacją. W niektórych przypadkach, gdy kod jest często ponownie uruchamiany, zapisanie jednej operacji (pomnożonej przez wiele razy) może spowodować ogólny wzrost prędkości. (Na przykład, jeśli zapisujesz jedną operację na piksel ekranu megapikselowego, to właśnie zapisałeś milion operacji. Pomnożony przez 60 ekranów na sekundę, a ty oszczędzasz jeszcze więcej operacji.)

Odpowiedź hvd zawiera dodatkowe przykłady.

TOOGAM
źródło
2

Tak, ponieważ jeden oznacza „ekwiwalent”, a drugi oznacza „ekwiwalent”, a te warunki wzajemnie się wykluczają. Wszelkie inne znaczenie dla tego operatora jest mylące i należy go unikać wszelkimi środkami.

oliora
źródło
Nie wykluczają się wzajemnie we wszystkich przypadkach. Na przykład dwie nieskończoności obie nie są sobie równe i nie są sobie równe.
vladon
@vladon może używać jednego zamiast drugiego w ogólnym przypadku ? Nie. Oznacza to, że po prostu nie są sobie równi. Cała reszta idzie na specjalną funkcję, a nie na operator == /! =
oliora
@ vladon, proszę, zamiast ogólnego przypadku przeczytaj wszystkie przypadki w mojej odpowiedzi.
oliora
@vladon O ile jest to prawdą w matematyce, czy możesz podać przykład, w którym a != bnie jest równy z !(a == b)tego powodu w C?
nitro2k01
2

Może nieporównywalne reguła, gdzie a != bbył fałszywy i a == bbył fałszywy jak trochę bezpaństwowcem.

if( !(a == b || a != b) ){
    // Stateless
}
ToñitoG
źródło
Jeśli chcesz zmienić logiczne symbole, to! ([A] || [B]) logicznie staje się ([! A] i [! B])
Thijser
Zwróć uwagę, że typ zwracany operator==()i operator!=()niekoniecznie bool, mogą być wyliczeniem, które obejmuje bezpaństwowców, jeśli chcesz, a mimo to operatory mogą być nadal zdefiniowane, więc (a != b) == !(a==b)zachowuje się.
lorro