Dlaczego logiczny operator NOT w językach C jest „!”, A nie „~~”?

39

W przypadku operatorów binarnych mamy zarówno operatory bitowe, jak i logiczne:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

NIE (jednoargumentowy operator) zachowuje się jednak inaczej. Jest ~ za bitowe i! dla logiki.

Rozumiem, że NIE jest operacją jednoargumentową w przeciwieństwie do AND i OR, ale nie mogę wymyślić powodu, dla którego projektanci postanowili odejść od zasady, że single jest bitowe, a double logiczne, i zamiast tego wybrali inną postać. Wydaje mi się, że można go źle odczytać, jak operację podwójnie bitową, która zawsze zwraca wartość argumentu. Ale to nie wydaje mi się prawdziwym problemem.

Czy brakuje mi powodu?

Martin Maat
źródło
7
Ponieważ jeśli !! oznaczało logiczne, że nie, jak zamienić 42 w 1? :)
candied_orange
9
Czy ~~nie byłoby bardziej spójne dla logicznego NOT, jeśli podążasz za wzorem, że operator logiczny jest podwojeniem operatora bitowego?
Bart van Ingen Schenau
9
Po pierwsze, gdyby było to dla spójności, byłoby to ~ i ~~ Podwojenie i / lub jest związane z zwarciem; a logiczne nie ma zwarcia.
Christophe
3
Podejrzewam, że podstawowym powodem projektowania jest przejrzystość i rozróżnienie w typowych przypadkach użycia. Operatory binarne (to znaczy dwupandandowe) są niepoprawne (i zwykle są oddzielane spacjami), podczas gdy operatory jednoargumentowe są prefiksami (i zwykle nie mają odstępów).
Steve
7
Jak niektóre komentarze już wspomniały (i dla tych, którzy nie chcą podążać za tym linkiem , !!foojest to nierzadki (niezbyt często?) Idiom. Normalizuje zero lub niezerowy argument na 0lub 1.
Keith Thompson

Odpowiedzi:

108

O dziwo, historia języka programowania w stylu C nie zaczyna się od C.

Dennis Ritchie wyjaśnia również wyzwaniom urodzenia C w tym artykule .

Czytając go, staje się oczywiste, że C odziedziczył część swojego projektu językowego od swojego poprzednika BCPL , a zwłaszcza od operatorów. Sekcja „Noworodkowy C” wyżej wymienionego artykułu wyjaśnia, w jaki sposób BCPL &i |zostały wzbogacone o dwóch nowych operatorów &&i ||. Przyczyny były:

  • wymagany był inny priorytet ze względu na jego użycie w połączeniu z ==
  • inna logika oceny: ocena od lewej do prawej ze zwarciem (tzn. kiedy ajest falsewłączona a&&b, bnie jest analizowana).

Co ciekawe, to podwojenie nie powoduje żadnych dwuznaczności dla czytelnika: a && bnie będzie interpretowane jako a(&(&b)). Z parsującego punktu widzenia nie ma również dwuznaczności: &bmoże mieć sens, jeśli bbyłaby wartością, ale byłby to wskaźnik, podczas gdy bitowe &wymagałoby operandu całkowitego, więc logiczne AND byłoby jedynym rozsądnym wyborem.

BCPL jest już używany ~do bitowej negacji. Tak więc z punktu widzenia spójności można było podwoić, aby nadać ~~logiczne znaczenie. Niestety byłoby to niezwykle dwuznaczne, ponieważ ~jest to jednoargumentowy operator: ~~bmoże również oznaczać ~(~b)). Właśnie dlatego musiał zostać wybrany inny symbol brakującej negacji.

Christophe
źródło
10
Analizator składni nie jest w stanie ujednoznacznić tych dwóch sytuacji, dlatego projektanci języków muszą to zrobić.
BobDalgleish
16
@ Steve: Rzeczywiście, istnieje wiele podobnych problemów już w językach C i C-podobnych. Kiedy parser widzi, (t)+1że jest to dodatek (t)i 1czy jest to obsada +1typu t? Projekt w C ++ musiał rozwiązać problem leksykania szablonów zawierających >>poprawnie. I tak dalej.
Eric Lippert
6
@ user2357112 Myślę, że chodzi o to, że w porządku jest, aby tokenizer ślepo traktował &&jako pojedynczy &&token, a nie jako dwa &tokeny, ponieważ a & (&b)interpretacja nie jest rozsądną rzeczą do napisania, więc człowiek nigdy by nie miał tego na myśli i byłby zaskoczony kompilator traktuje to jak a && b. Zważywszy, że zarówno !(!a)i !!amożliwe są rzeczy dla człowieka znaczyć, więc jest to zły pomysł, kompilator rozwiązać niejednoznaczność z dowolnej reguły tokenizacja poziomu.
Ben
17
!!jest nie tylko możliwe / rozsądne do napisania, ale kanoniczny idiom „konwersja na boolean”.
R ..
4
Myślę, że dan04 odnosi się do niejednoznaczności --avs -(-a), które są poprawne składniowo, ale mają inną semantykę.
Ruslan
49

Nie mogę wymyślić powodu, dla którego projektanci postanowili odejść od zasady, że singiel jest bitowy, a podwójność jest tutaj logiczna,

Po pierwsze, to nie jest zasada; kiedy sobie to uświadomisz, ma to większy sens.

Lepszym sposobem myślenia o &vs &&nie jest binarny i logiczny . Lepszym sposobem jest postrzeganie ich jako chętnych i leniwych . &Operator wykonuje lewej i prawej stronie, a następnie oblicza wynik. &&Operator wykonuje lewą stronę, a następnie wykonuje prawą stronę tylko jeśli jest to konieczne, aby obliczyć wynik.

Co więcej, zamiast myśleć o „binarnym” i „logicznym”, pomyśl o tym, co naprawdę się dzieje. Wersja „binarna” po prostu wykonuje operację boolowską na tablicy logicznej, która została upakowana w słowo .

Złóżmy to razem. Czy ma sens robienie leniwej operacji na szeregu booleanów ? Nie, ponieważ nie ma „lewej strony” do sprawdzenia w pierwszej kolejności. Najpierw są 32 „lewe strony”. Ograniczamy więc leniwe operacje do jednego logicznego loga i stąd twoja intuicja, że ​​jedna z nich jest „binarna”, a druga „logiczna”, ale jest to konsekwencja projektu, a nie samego projektu!

A kiedy pomyślisz o tym w ten sposób, staje się jasne, dlaczego nie ma !!i nie ma ^^. Żaden z tych operatorów nie ma właściwości, którą można pominąć analizując jeden z operandów; nie ma „leniwego” notlub xor.

Inne języki wyjaśniają to; niektóre języki andoznaczają na przykład „chętny i”, ale and alsona przykład „leniwy i”. Inne języki również to wyjaśniają &i &&nie są „binarne” ani „logiczne”; na przykład w języku C # obie wersje mogą przyjmować booleany jako operandy.

Eric Lippert
źródło
2
Dziękuję Ci. To dla mnie prawdziwy otwieracz do oczu. Szkoda, że ​​nie mogę zaakceptować dwóch odpowiedzi.
Martin Maat
10
Nie sądzę, że to dobry sposób na myślenie &i &&. Podczas gdy zapał jest jedną z różnic między &i &&, &zachowuje się zupełnie inaczej niż chętna wersja &&, szczególnie w językach, w których &&obsługuje typy inne niż dedykowany typ logiczny.
user2357112 obsługuje Monikę
14
Na przykład w C i C ++ 1 & 2ma zupełnie inny wynik niż 1 && 2.
user2357112 obsługuje Monikę
7
@ZizyArcher: Jak zauważyłem w powyższym komentarzu, decyzja o pominięciu booltypu w C wywołuje efekt domina. Potrzebujemy obu, !a ~ponieważ jeden oznacza „traktuj int jako pojedynczy log boolowski”, a drugi oznacza „traktuj int jako spakowaną tablicę boolean”. Jeśli masz oddzielne typy bool i int, możesz mieć tylko jednego operatora, co moim zdaniem byłoby lepszym projektem, ale spóźniliśmy się prawie o 50 lat. C # zachowuje ten projekt dla znajomości.
Eric Lippert
3
@ Steve: Jeśli odpowiedź wydaje się absurdalna, to gdzieś przedstawiłem źle wyrażony argument i nie powinniśmy polegać na argumentie władzy. Czy możesz powiedzieć coś więcej na temat absurdu?
Eric Lippert
21

TL; DR

C odziedziczył operatory !i ~z innego języka. Zarówno &&i ||dodano lat później przez inną osobę.

Długa odpowiedź

Historycznie C rozwijał się z wczesnych języków B, które były oparte na BCPL, która była oparta na CPL, która była oparta na Algolu.

Algol , prawnuk C ++, Java i C #, zdefiniował prawdę i fałsz w sposób, który stał się intuicyjny dla programistów: „wartości prawdy, które, uważane za liczbę binarną (prawda odpowiada 1, a fałsz 0), to samo co wewnętrzna wartość całkowita ”. Jednak jedną wadą tego jest to, że logiczne i bitowe nie może być tą samą operacją: na każdym nowoczesnym komputerze ~0równa się -1 zamiast 1 i ~1równa się -2 zamiast 0. (Nawet na około sześćdziesięcioletnim komputerze mainframe, gdzie ~0reprezentuje - 0 lub INT_MIN, ~0 != 1na każdym procesorze, jaki kiedykolwiek wyprodukowano, a standard języka C wymagał tego od wielu lat, podczas gdy większość jego języków potomnych nie zadaje sobie nawet trudu, aby obsługiwać znak i wielkość lub uzupełnienie.)

Algol obejrzał ten problem, mając różne tryby i różnie interpretując operatory w trybie logicznym i integralnym. Oznacza to, że operacja bitowa dotyczyła typów całkowitych, a operacja logiczna dotyczyła typów logicznych.

BCPL miał osobny typ logiczny, ale pojedynczy notoperator , zarówno logiczny, jak i bitowy. Sposób, w jaki ten wczesny prekursor C sprawił, że ta praca była:

Wartość prawdy jest wzorem złożonym całkowicie z nich; wartość false wynosi zero.

Zauważ, że true = ~ false

(Zauważysz, że termin wartość ewoluował, aby oznaczać coś zupełnie innego w językach rodziny C. Dzisiaj nazwalibyśmy to „reprezentacją obiektu” w C.)

Ta definicja pozwoliłaby logicznie i bitowo nie używać tej samej instrukcji języka maszynowego. Gdyby C poszedł tą drogą, pliki nagłówkowe powiedziałby cały świat #define TRUE -1.

Ale język programowania B był słabo napisany i nie miał typów boolowskich ani nawet zmiennoprzecinkowych. Wszystko było równoważne z intjego następcą, C. To sprawiło, że dobrym pomysłem było, aby język zdefiniował, co się stanie, gdy program użyje wartości innej niż prawda lub fałsz jako wartości logicznej. Najpierw zdefiniował prawdziwe wyrażenie jako „nie równe zero”. Było to skuteczne na minikomputerach, na których działało, które miały flagę zero procesora.

Istniała wówczas alternatywa: te same procesory miały również flagę ujemną, a wartość prawdy BCPL wynosiła -1, więc B mógł zamiast tego zdefiniować wszystkie liczby ujemne jako prawdziwe, a wszystkie nieujemne jako fałsz. (Jest jedna pozostałość tego podejścia: wiele wywołań systemowych w systemie UNIX, opracowanych przez te same osoby w tym samym czasie, definiuje wszystkie kody błędów jako liczby całkowite ujemne. Wiele wywołań systemowych zwraca jedną z kilku różnych wartości ujemnych po awarii.) bądźcie wdzięczni: mogło być gorzej!

Ale zdefiniowanie TRUEjako 1i FALSEtak jak 0w B oznaczało, że tożsamość true = ~ falsejuż nie zachowała, i porzuciło silne pisanie, które pozwoliło Algolowi rozróżnić wyrażenia bitowe i logiczne. Wymagało to nowego logicznego operatora, który nie jest logiczny, a projektanci wybrali !, być może dlatego, że już nie był równy !=, który wygląda jak pionowy pasek przez znak równości. Oni nie poszli tą samą konwencję, &&albo ||dlatego, że ani jedno, jeszcze istniał.

Zapewne powinny: &operator w B jest uszkodzony zgodnie z przeznaczeniem. W B i C, w 1 & 2 == FALSEchoć 1i 2są obie wartości truthy, i nie ma intuicyjny sposób wyrazić logiczną operację w B. To była jedna pomyłka C starał się częściowo naprawić dodając &&i ||, ale głównym problemem w tym czasie był na w końcu doszło do zwarcia i przyspieszyło działanie programów. Dowodem na to jest to, że nie ma ^^: 1 ^ 2jest prawdziwą wartością, chociaż oba jej operandy są prawdziwe, ale nie może skorzystać na zwarciu.

Davislor
źródło
4
+1. Myślę, że to całkiem niezła wycieczka z przewodnikiem po ewolucji tych operatorów.
Steve
BTW, maszyny do znakowania / wielkości i dopełniacza również wymagają oddzielnej negacji bitowej vs. logicznej, nawet jeśli dane wejściowe są już logiczne. ~0(wszystkie ustawione bity) to zero ujemne dopełniacza (lub reprezentacja pułapki). Znak / jasność ~0jest liczbą ujemną o maksymalnej wielkości.
Peter Cordes
@PeterCordes Masz absolutną rację. Właśnie skupiłem się na dwóch komplementarnych maszynach, ponieważ są one o wiele ważniejsze. Może warto przypis.
Davislor
Myślę, że mój komentarz jest wystarczający, ale tak, być może nawias (nie działa dla uzupełnienia 1 ani znaku / wielkości) byłby dobrą edycją.
Peter Cordes