Warum ist der logische NOT-Operator in C-Sprachen "!" Und nicht "~~"?

39

Für binäre Operatoren haben wir sowohl bitweise als auch logische Operatoren:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

NOT (ein unärer Operator) verhält sich jedoch anders. Es gibt ~ für bitweise und! für logisch.

Ich erkenne, dass NOT eine unäre Operation im Gegensatz zu AND und OR ist, aber ich kann mir keinen Grund vorstellen, warum die Designer beschlossen haben, von dem Prinzip abzuweichen, dass single bitweise und double logisch ist, und stattdessen ein anderes Zeichen gewählt haben. Sie könnten es falsch lesen, wie eine doppelte bitweise Operation, die immer den Operandenwert zurückgibt. Das scheint mir aber kein wirkliches Problem zu sein.

Gibt es einen Grund, warum ich vermisse?

Martin Maat
quelle
7
Weil wenn !! meinte logisch nicht, wie würde ich 42 in 1 verwandeln? :)
candied_orange
9
Wäre das ~~logische NICHT dann nicht konsistenter, wenn Sie dem Muster folgen, dass der logische Operator eine Verdoppelung des bitweisen Operators ist?
Bart van Ingen Schenau
9
Erstens wäre es aus Gründen der Konsistenz ~ und ~~ gewesen. Die Verdoppelung von und und oder ist mit dem Kurzschluss verbunden. und das logische nicht hat keinen kurzschluss.
Christophe
3
Ich vermute, dass der zugrunde liegende Designgrund in den typischen Anwendungsfällen die visuelle Klarheit und Unterscheidung ist. Die binären Operatoren (dh Operanden mit zwei Operanden) sind Infix-Operatoren (und werden in der Regel durch Leerzeichen getrennt), während die unären Operatoren Präfix-Operatoren sind (und in der Regel keine Abstände aufweisen).
Steve
7
Wie einige Kommentare bereits angedeutet haben (und für diejenigen, die diesem Link nicht folgen möchten , !!fooist dies eine nicht ungewöhnliche (nicht gebräuchliche?) Redewendung. Sie normalisiert ein Null- oder Nicht-Null-Argument zu 0oder 1.
Keith Thompson

Antworten:

108

Seltsamerweise beginnt die Geschichte der Programmiersprache C nicht mit C.

Dennis Ritchie erklärt in diesem Artikel die Herausforderungen von Cs Geburt .

Beim Lesen wird deutlich, dass C einen Teil seines Sprachentwurfs von seinem Vorgänger BCPL und insbesondere von den Operatoren geerbt hat . Der Abschnitt "Neugeborene C" des oben genannten Artikels erklärt, wie BCPL &und |mit zwei neuen Operatoren &&und angereichert wurden ||. Die Gründe waren:

  • Aufgrund der Verwendung in Kombination mit war eine andere Priorität erforderlich ==
  • verschiedene Auswertungslogik: von links nach rechts Auswertung mit Kurzschluss (dh wenn aist falsein a&&b, bnicht ausgewertet).

Interessanterweise erzeugt diese Verdoppelung keine Mehrdeutigkeit für den Leser: a && bwird nicht als falsch interpretiert a(&(&b)). Aus der Sicht der Syntaxanalyse gibt es auch keine Mehrdeutigkeit: &bWenn bein l-Wert sinnvoll wäre , wäre dies ein Zeiger, wohingegen bitweise &ein ganzzahliger Operand erforderlich ist. Daher ist das logische UND die einzig sinnvolle Wahl.

BCPL wurde bereits ~für die bitweise Negation verwendet. Unter dem Gesichtspunkt der Konsistenz wäre es also möglich gewesen, a ~~zu verdoppeln , um ihm seine logische Bedeutung zu verleihen. Dies wäre leider äußerst zweideutig gewesen, da ~es sich um einen unären Operator handelt: ~~bkönnte auch bedeuten ~(~b)). Deshalb musste für die fehlende Negation ein anderes Symbol gewählt werden.

Christophe
quelle
10
Der Parser ist nicht in der Lage, die beiden Situationen zu unterscheiden, daher müssen die Sprachdesigner dies tun.
BobDalgleish
16
@Steve: In der Tat gibt es viele ähnliche Probleme bereits in C- und C-ähnlichen Sprachen. Wenn der Parser sieht, (t)+1ist das eine Ergänzung von (t)und 1oder ist es eine Besetzung vom +1Typ t? C ++ Design musste das Problem lösen, wie man Vorlagen, die >>richtig enthalten, lexiert. Und so weiter.
Eric Lippert
6
@ user2357112 Ich denke, der Punkt ist, dass es in Ordnung ist, den Tokenizer blind &&als ein einzelnes &&Token und nicht als zwei &Token zu betrachten, da die a & (&b)Interpretation nicht sinnvoll zu schreiben ist. Ein Mensch hätte das also niemals gemeint und wäre von ihm überrascht worden der Compiler behandelt es als a && b. Während die beiden !(!a)und !!asind möglich , Dinge für einen Menschen zu bedeuten, so ist es eine schlechte Idee für die Compiler die Zweideutigkeit mit einer beliebigen tokenization-Level - Regel zu lösen.
Ben
17
!!ist nicht nur möglich / sinnvoll zu schreiben, sondern die kanonische "umwandeln in boolesche" Redewendung.
R ..
4
Ich denke, dan04 bezieht sich auf die Mehrdeutigkeit von --avs -(-a), die beide syntaktisch gültig sind, aber unterschiedliche Semantik haben.
Ruslan
49

Ich kann mir keinen Grund vorstellen, warum die Designer sich entschieden haben, von dem Prinzip abzuweichen, dass hier einfach bitweise und doppelt logisch ist.

Das ist nicht das Prinzip an erster Stelle; Sobald Sie das merken, macht es mehr Sinn.

Die bessere Art, an &vs zu denken, &&ist nicht binär und boolesch . Der bessere Weg ist, sie als eifrig und faul zu betrachten . Der &Bediener führt die linke und rechte Seite aus und berechnet dann das Ergebnis. Der &&Operator führt die linke Seite und dann die rechte Seite nur dann aus, wenn dies zur Berechnung des Ergebnisses erforderlich ist.

Anstatt über "binär" und "boolesch" nachzudenken, sollten Sie darüber nachdenken, was wirklich passiert. Die "binäre" Version führt nur die Boolesche Operation für ein Array von Booleschen Werten aus, die in ein Wort gepackt wurden .

Also lasst es uns zusammensetzen. Ist es sinnvoll, eine verzögerte Operation mit einer Reihe von Booleschen Werten durchzuführen ? Nein, da es keine "linke Seite" gibt, die zuerst überprüft werden muss. Es gibt 32 "linke Seiten", die zuerst überprüft werden müssen. Wir beschränken die faulen Operationen also auf einen einzelnen Booleschen Wert, und daher kommt Ihre Intuition, dass einer von ihnen "binär" und einer "Boolescher Wert" ist, doch das ist eine Konsequenz des Designs, nicht des Designs selbst!

Und wenn man so denkt, wird klar, warum es kein !!und kein gibt ^^. Keiner dieser Operatoren verfügt über die Eigenschaft, dass Sie die Analyse eines der Operanden überspringen können. es gibt kein "faul" notoder xor.

Andere Sprachen machen dies deutlicher. Einige Sprachen andbedeuten zum Beispiel "eifrig und", aber and alsoauch "faul und". Und andere Sprachen machen es auch klarer &und &&sind nicht "binär" und "boolesch"; In C # können beispielsweise beide Versionen Boolesche Werte als Operanden verwenden.

Eric Lippert
quelle
2
Vielen Dank. Dies ist der wahre Augenöffner für mich. Schade, dass ich zwei Antworten nicht akzeptieren kann.
Martin Maat
10
Ich denke nicht, dass dies eine gute Art ist, an &und zu denken &&. Während Eifer einer der Unterschiede zwischen &und ist &&, &verhält es sich ganz anders als eine eifrige Version von &&, insbesondere in Sprachen, in denen &&andere Typen als ein dedizierter Boolescher Typ unterstützt werden.
user2357112 unterstützt Monica
14
Beispielsweise hat in C und C ++ 1 & 2ein völlig anderes Ergebnis als 1 && 2.
user2357112 unterstützt Monica
7
@ZizyArcher: Wie ich im obigen Kommentar bemerkt habe, hat die Entscheidung, einen boolTyp in C wegzulassen, Konsequenzen . Wir brauchen beides !und ~weil man "ein Int als einen einzelnen Booleschen Wert behandeln" und man "ein Int als ein gepacktes Array von Booleschen Werten behandeln" bedeutet. Wenn Sie getrennte Bool- und Int-Typen haben, können Sie nur einen Operator haben, was meiner Meinung nach das bessere Design gewesen wäre, aber wir sind fast 50 Jahre zu spät dran. C # bewahrt dieses Design für die Vertrautheit.
Eric Lippert
3
@Steve: Wenn die Antwort absurd erscheint, habe ich irgendwo ein schlecht ausgedrücktes Argument vorgebracht, und wir sollten uns nicht auf ein Argument der Behörde stützen. Können Sie mehr darüber sagen, was daran absurd erscheint?
Eric Lippert
21

TL; DR

C hat die Operatoren !und ~von einer anderen Sprache geerbt . Beide &&und ||wurden Jahre später von einer anderen Person hinzugefügt.

Lange Antwort

Historisch gesehen entwickelte sich C aus den frühen Sprachen B, die auf BCPL beruhten, das auf CPL beruhte, das auf Algol beruhte.

Algol , der Urgroßvater von C ++, Java und C #, definierte wahr und falsch auf eine Weise, die sich für Programmierer intuitiv anfühlte: „Wahrheitswerte, die als Binärzahl betrachtet werden (wahr entspricht 1 und falsch ist 0), sind das gleiche wie der innere Integralwert ”. Ein Nachteil davon ist jedoch, dass logisch und bitweise nicht die gleiche Operation sein kann: Auf jedem modernen Computer ist ~0-1 anstelle von 1 und ~1-2 anstelle von 0. (Sogar auf einem 60 Jahre alten Mainframe ist ~0- 0 oder INT_MIN, ~0 != 1auf jeder jemals hergestellten CPU, und der C-Sprachstandard verlangt dies seit vielen Jahren, während sich die meisten seiner Tochtersprachen überhaupt nicht darum kümmern, Vorzeichen und Größe oder das eigene Komplement zu unterstützen.)

Algol hat dies umgangen, indem es verschiedene Modi hatte und die Operatoren im Booleschen und im Integralmodus unterschiedlich interpretierte. Das heißt, eine bitweise Operation war eine für Integer-Typen, und eine logische Operation war eine für Boolesche Typen.

BCPL hatte einen separaten Booleschen Typ, aber einen einzelnen notOperator , sowohl für bitweise als auch für logische nicht. Die Art und Weise, wie dieser frühe Vorläufer von C diese Arbeit machte, war:

Der R-Wert von true ist ein Bitmuster, das vollständig aus Einsen besteht. Der R-Wert von false ist Null.

Beachten Sie, dass true = ~ false

(Sie werden feststellen , dass sich der Begriff rvalue in den Sprachen der C-Familie zu einem völlig anderen Begriff entwickelt hat. Wir würden das heute als „Objektrepräsentation“ in C bezeichnen.)

Diese Definition würde es logisch und bitweise ermöglichen, nicht dieselbe maschinensprachliche Anweisung zu verwenden. Wenn C diesen Weg gegangen wäre, würden Header-Dateien auf der ganzen Welt sagen #define TRUE -1.

Aber die Programmiersprache B war schwach typisiert und hatte weder Boolesche noch Gleitkommatypen. Alles war das Äquivalent zu intseinem Nachfolger C. Dies machte es für die Sprache zu einer guten Idee, zu definieren, was passiert ist, wenn ein Programm einen anderen Wert als true oder false als logischen Wert verwendet. Zuerst definierte es einen wahrheitsgemäßen Ausdruck als "ungleich Null". Dies war auf den Minicomputern, auf denen es lief und die ein CPU-Null-Flag hatten, effizient.

Damals gab es eine Alternative: Dieselben CPUs hatten auch ein negatives Flag, und der Wahrheitswert von BCPL war -1, sodass B möglicherweise stattdessen alle negativen Zahlen als wahr und alle nicht negativen Zahlen als falsch definiert hat. (Es gibt einen Rest dieses Ansatzes: Viele Systemaufrufe in UNIX, die von denselben Personen zur selben Zeit entwickelt wurden, definieren alle Fehlercodes als negative Ganzzahlen. Viele ihrer Systemaufrufe geben bei einem Fehler einen von mehreren negativen Werten zurück.) Also sei dankbar: es hätte schlimmer kommen können!

Aber die Definition TRUEwie 1und FALSEwie 0in B bedeutet , dass die Identität true = ~ falsenicht mehr zu halten, und sie hatte die starke Typisierung fallen gelassen , die Algol eindeutig zu machen zwischen bitweise und logische Ausdrücke erlaubt. Das erforderte einen neuen logisch-nicht-Operator, und die Designer wählten ihn aus !, möglicherweise, weil es bereits !=einen ungleichen Operator gab, der durch ein Gleichheitszeichen wie ein vertikaler Strich aussieht. Sie folgten nicht der gleichen Konvention wie &&oder ||weil es noch keine gab.

Möglicherweise sollte dies der Fall sein: Der &Operator in B ist fehlerhaft. In B und C, 1 & 2 == FALSEobwohl 1und 2beiden truthy Werte sind, und es gibt keine intuitive Art und Weise die logische Operation in B. um auszudrücken , dass ein Fehler C versuchte , war teilweise zu korrigieren , indem das Hinzufügen &&und ||, aber das Hauptanliegen war zu der Zeit zu Endlich kann der Kurzschluss funktionieren und Programme können schneller ausgeführt werden. Der Beweis dafür ist, dass es kein ^^: gibt, 1 ^ 2ist ein wahrer Wert, obwohl beide Operanden wahr sind, aber es kann nicht von einem Kurzschluss profitieren.

Davislor
quelle
4
+1. Ich denke, dies ist eine ziemlich gute Führung durch die Entwicklung dieser Betreiber.
Steve
BTW-, Vorzeichen- / Größen- und Einerkomplementierungsmaschinen benötigen ebenfalls eine getrennte bitweise vs. logische Negation, auch wenn die Eingabe bereits boolesch ist. ~0(alle gesetzten Bits) ist eine negative Null im Komplement (oder eine Trap-Darstellung). Vorzeichen / Größe ~0ist eine negative Zahl mit maximaler Größe.
Peter Cordes
@PeterCordes Du hast absolut recht. Ich habe mich nur auf Zweierkomplement-Maschinen konzentriert, weil sie viel wichtiger sind. Vielleicht ist es eine Fußnote wert.
Davislor
Ich denke, mein Kommentar ist ausreichend, aber ja, vielleicht wäre eine Klammer (funktioniert auch nicht für das Komplement oder das Vorzeichen / die Größe von 1) eine gute Bearbeitung.
Peter Cordes