Perché l'operatore logico NOT nei linguaggi in stile C “!” E non “~~”?


40

Per gli operatori binari abbiamo operatori sia bit a bit che logici:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

NOT (un operatore unario) si comporta diversamente. C'è ~ per bitwise e! per logica.

Riconosco che NOT è un'operazione unaria al contrario di AND e OR, ma non riesco a pensare a una ragione per cui i designer hanno scelto di deviare dal principio che single è bit a bit e double è logico qui, e invece ha scelto un personaggio diverso. Immagino che potresti leggerlo male, come una doppia operazione bit a bit che restituirebbe sempre il valore dell'operando. Ma questo non mi sembra un vero problema.

C'è un motivo per cui mi manca?


7
Perchè se !! significava logicamente no, come avrei trasformato 42 in 1? :)
candied_orange

9
Non sarebbe ~~quindi più coerente per il NOT logico, se seguissi il modello secondo cui l'operatore logico raddoppia l'operatore bit a bit?
Bart van Ingen Schenau,

9
Primo, se fosse stato per coerenza sarebbe stato ~ e ~~ Il raddoppio di e ed o è associato al corto circuito; e il logico non ha un corto circuito.
Christophe,

3
Ho il sospetto che il motivo progettuale sottostante sia la chiarezza visiva e la distinzione, nei casi d'uso tipici. Gli operatori binari (ovvero due operandi) sono infissi (e tendono ad essere separati da spazi), mentre gli operatori unari sono prefissi (e tendono a non essere spaziati).
Steve,

7
Come alcuni commenti hanno già fatto allusione (e per coloro che non vogliono seguire questo link , !!fooè un linguaggio non insolito (non comune?) Che normalizza un argomento zero o diverso da zero a 0o 1.
Keith Thompson

Risposte:


110

Stranamente, la storia del linguaggio di programmazione in stile C non inizia con C.

Dennis Ritchie spiega bene le sfide della nascita di C in questo articolo .

Durante la lettura, diventa evidente che C ha ereditato una parte del suo linguaggio dal suo predecessore BCPL , e in particolare dagli operatori. La sezione "Neonatal C" del suddetto articolo spiega come i BCPL &e |sono stati arricchiti con due nuovi operatori &&e ||. Le ragioni erano:

  • era necessaria una priorità diversa a causa del suo uso in combinazione con ==
  • diversa logica di valutazione: valutazione da sinistra a destra con corto circuito (cioè quando aè falsedentro a&&b, bnon viene valutata).

È interessante notare che questo raddoppio non crea alcuna ambiguità per il lettore: a && bnon verrà interpretato erroneamente come a(&(&b)). Da un punto di vista dell'analisi, non vi è alcuna ambiguità: &bpotrebbe avere senso se bfosse un lvalue, ma sarebbe un puntatore mentre il bit a bit &richiederebbe un operando intero, quindi l'AND logico sarebbe l'unica scelta ragionevole.

BCPL già utilizzato ~per negazione bit a bit. Quindi, da un punto di vista della coerenza, avrebbe potuto essere raddoppiato per dargli un ~~significato logico. Purtroppo questo sarebbe stato estremamente ambiguo poiché ~è un operatore unario: ~~bpotrebbe anche significare ~(~b)). Ecco perché un altro simbolo doveva essere scelto per la negazione mancante.


10
Il parser non è in grado di chiarire le due situazioni, quindi i progettisti del linguaggio devono farlo.
BobDalgleish,

16
@Steve: In effetti, ci sono già molti problemi simili nei linguaggi C e C-like. Quando il parser vede (t)+1è che l'aggiunta di (t)e 1o è un cast di +1per tipo t? Il design C ++ ha dovuto risolvere il problema di come creare >>correttamente i template di lex . E così via.
Eric Lippert,

6
@ user2357112 Penso che il punto sia che va bene che il tokenizer prenda ciecamente &&come un singolo &&token e non come due &token, perché l' a & (&b)interpretazione non è una cosa ragionevole da scrivere, quindi un essere umano non lo avrebbe mai voluto dire e ne sarei sorpreso il compilatore trattandolo come a && b. Considerando che tanto !(!a)e !!asono possibili cose per un essere umano a significare, quindi è una cattiva idea per il compilatore per risolvere l'ambiguità con una regola a livello di tokenizzazione arbitrario.
Ben

18
!!non è solo possibile / ragionevole scrivere, ma il linguaggio canonico "converti in booleano".
R ..

4
Penso che dan04 si riferisca all'ambiguità di --avs -(-a), entrambi validi sintatticamente ma con semantica diversa.
Ruslan,

49

Non riesco a pensare a una ragione per cui i designer hanno scelto di discostarsi dal principio che single è bit a bit e double è logico qui,

Questo non è il principio in primo luogo; una volta che lo capisci, ha più senso.

Il modo migliore di pensare & vs &&non è binario e booleano . Il modo migliore è di considerarli desiderosi e pigri . L' &operatore esegue il lato sinistro e destro e quindi calcola il risultato. L' &&operatore esegue il lato sinistro, quindi esegue il lato destro solo se necessario per calcolare il risultato.

Inoltre, invece di pensare a "binario" e "booleano", pensa a ciò che sta realmente accadendo. La versione "binaria" sta semplicemente eseguendo l'operazione booleana su un array di booleani che è stato impacchettato in una parola .

Quindi mettiamolo insieme. Ha senso fare un'operazione pigra su un array di booleani ? No, perché non esiste un "lato sinistro" da controllare per primo. Ci sono 32 "lati a sinistra" da controllare per primi. Quindi limitiamo le operazioni pigre a un singolo booleano, ed ecco da dove viene la tua intuizione che uno di essi è "binario" e uno è "booleano", ma questa è una conseguenza del design, non del design stesso!

E quando ci pensi in questo modo, diventa chiaro perché non ci sono né !!no ^^. Nessuno di questi operatori ha la proprietà che è possibile saltare analizzando uno degli operandi; non c'è "pigro" noto xor.

Altre lingue lo rendono più chiaro; alcune lingue usanoand per dire "desideroso e" ma and alsoper dire "pigro e", per esempio. E altre lingue lo rendono anche più chiaro &e &&non sono "binari" e "booleani"; in C # per esempio, entrambe le versioni possono prendere i booleani come operandi.


2
Grazie. Questo è il vero colpo d'occhio per me. Peccato non posso accettare due risposte.
Martin Maat,

11
Non penso che questo sia un buon modo di pensare &e &&. Mentre l'entusiasmo è una delle differenze tra &e &&, &si comporta in modo completamente diverso da una versione desiderosa di &&, in particolare nelle lingue in cui &&supporta tipi diversi da un tipo booleano dedicato.
user2357112 supporta Monica il

14
Ad esempio, in C e C ++, 1 & 2ha un risultato completamente diverso da 1 && 2.
user2357112 supporta Monica il

7
@ZizyArcher: Come ho notato nel commento sopra, la decisione di omettere un booltipo in C ha effetti a catena. Abbiamo bisogno di entrambi !e ~perché uno significa "tratta un int come un singolo booleano" e uno significa "tratta un int come un array compresso di booleani". Se hai tipi bool e int separati, puoi avere un solo operatore, che secondo me sarebbe stato il miglior design, ma siamo in ritardo di quasi 50 anni. C # conserva questo design per familiarità.
Eric Lippert,

3
@Steve: se la risposta sembra assurda, allora ho fatto un argomento scarsamente espresso da qualche parte, e non dovremmo fare affidamento su un argomento dell'autorità. Puoi dire di più su ciò che sembra assurdo al riguardo?
Eric Lippert,

21

TL; DR

C ha ereditato gli operatori !e ~da un'altra lingua. Tutti e due&& e ||sono stati aggiunti anni dopo da una persona diversa.

Risposta lunga

Storicamente, C si è sviluppato dalle prime lingue B, che era basato su BCPL, che era basato su CPL, che era basato su Algol.

Algol , il bisnonno di C ++, Java e C #, ha definito vero e falso in un modo che è diventato intuitivo per i programmatori: "valori di verità che, considerati come un numero binario (vero corrispondente a 1 e falso a 0), è lo stesso del valore integrale intrinseco ”. Tuttavia, uno svantaggio di questo è che logico e bit a bit non può essere la stessa operazione: su qualsiasi computer moderno, è ~0uguale a -1 anziché a 1 e ~1uguale a -2 anziché a 0. (Anche su un mainframe di sessant'anni dove ~0rappresenta - 0 o INT_MIN, ~0 != 1su ogni CPU mai realizzata, e lo standard del linguaggio C lo richiede da molti anni, mentre la maggior parte delle sue lingue figlie non si preoccupano nemmeno di supportare il segno-magnitudine o il complemento.

Algol ha risolto il problema disponendo di modalità diverse e interpretando gli operatori in modo diverso in modalità booleana e integrale. Ossia, un'operazione bit a bit era una sui tipi interi e un'operazione logica era una sui tipi booleani.

BCPL aveva un tipo booleano separato, ma un singolo notoperatore , sia per bit che per logico. Il modo in cui questo precursore precoce di C fece quel lavoro fu:

Il valore di vero è uno schema di bit interamente composto da uno; il valore di false è zero.

Nota che true = ~ false

(Noterai che il termine rvalue si è evoluto per significare qualcosa di completamente diverso nei linguaggi della famiglia C. Oggi chiameremmo "la rappresentazione dell'oggetto" in C.)

Questa definizione consentirebbe logicamente e bit a bit di non utilizzare la stessa istruzione in linguaggio macchina. Se C avesse seguito quella strada, direbbero i file header in tutto il mondo#define TRUE -1 .

Ma il linguaggio di programmazione B era tipizzato debolmente e non aveva tipi booleani o nemmeno a virgola mobile. Tutto era l'equivalente del intsuo successore, C. Questo ha reso una buona idea per il linguaggio definire cosa è successo quando un programma ha usato un valore diverso da vero o falso come valore logico. In primo luogo ha definito un'espressione veritiera come "non uguale a zero". Ciò era efficace sui minicomputer su cui era in esecuzione, che aveva un flag zero CPU.

All'epoca c'era un'alternativa: anche le stesse CPU avevano un flag negativo e il valore di verità di BCPL era -1, quindi B avrebbe potuto invece definire tutti i numeri negativi come veri e tutti i numeri non negativi come falsi. (C'è un residuo di questo approccio: UNIX, sviluppato dalle stesse persone contemporaneamente, definisce tutti i codici di errore come numeri interi negativi. Molte delle sue chiamate di sistema restituiscono uno dei diversi valori negativi in ​​caso di fallimento.) Quindi sii grato: avrebbe potuto essere peggio!

Ma definire TRUEcome 1e FALSEcome 0in B significava che l'identità true = ~ falsenon era più valida, e aveva lasciato cadere la tipizzazione forte che permetteva ad Algol di chiarire le espressioni logiche e bitrate. Ciò ha richiesto un nuovo operatore logico e non, e i progettisti hanno scelto !, forse perché non era già uguale !=, che sembra una specie di barra verticale attraverso un segno di uguale. Non hanno seguito la stessa convenzione &&o ||perché nessuno dei due esisteva ancora.

Probabilmente avrebbero dovuto: l' &operatore in B è rotto come previsto. In B e in C, 1 & 2 == FALSEanche se 1e 2sono entrambi valori truthy, e non c'è modo intuitivo per esprimere l'operazione logica in B. Questo è stato un errore C cercato di rimediare in parte aggiungendo &&e ||, ma la preoccupazione principale al momento era quello finalmente far funzionare i cortocircuiti e rendere i programmi più veloci. La prova di ciò è che non esiste ^^: 1 ^ 2è un valore veritiero anche se entrambi i suoi operandi sono veritieri, ma non può beneficiare del cortocircuito.


4
+1. Penso che questo sia un tour guidato abbastanza buono sull'evoluzione di questi operatori.
Steve

A proposito, segno / magnitudo e le macchine del proprio complemento necessitano anche di una negazione bit per bit rispetto a logica, anche se l'input è già booleanizzato. ~0(tutti i bit impostati) è lo zero negativo del proprio complemento (o una rappresentazione di trap). Segno / magnitudo ~0è un numero negativo con magnitudine massima.
Peter Cordes,

@PeterCordes Hai assolutamente ragione. Mi stavo solo concentrando su macchine a due complementi perché sono molto più importanti. Forse vale una nota a piè di pagina.
Davislor,

Penso che il mio commento sia sufficiente, ma sì, forse una parentesi (non funziona neanche per il complemento di 1 o il segno / magnitudine) sarebbe una buona modifica.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.