Por que o operador lógico NOT nas linguagens de estilo C é “!” E não “~~”?

40

Para operadores binários, temos operadores lógicos e bit a bit:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

NOT (um operador unário) se comporta de maneira diferente. Existe ~ para bit a bit e! para lógico.

Reconheço que NOT é uma operação unária, em oposição a AND e OR, mas não consigo pensar em uma razão pela qual os designers escolheram se desviar do princípio de que único é bit a bit e o dobro é lógico aqui, e optou por um personagem diferente. Eu acho que você pode ler errado, como uma operação bit a bit dupla que sempre retornaria o valor do operando. Mas isso não me parece um problema real.

Existe uma razão pela qual estou perdendo?

Martin Maat
fonte
7
Porque se !! não significava lógico, como eu transformaria 42 em 1? :)
candied_orange 30/09
9
Seria ~~, então, não ter sido mais consistente para NOT lógico, se você seguir o padrão que o operador lógico é uma duplicação do operador bit a bit?
Bart van Ingen Schenau 30/09
9
Primeiro, se fosse por consistência, teria sido ~ e ~~ A duplicação de e e ou está associada ao curto-circuito; e o não lógico não tem um curto-circuito.
Christophe
3
Suspeito que o motivo subjacente do design seja a clareza e distinção visual, nos casos de uso típicos. Os operadores binários (isto é, dois operandos) são infixos (e tendem a ser separados por espaços), enquanto os operadores unários são prefixos (e tendem a não ser espaçados).
Steve
7
Como alguns comentários já mencionaram (e para aqueles que não querem seguir este link ,!!foo é um não-incomum (não não é comum) idioma Normaliza um argumento de zero-ou-não-zero para?. 0Ou 1.
Keith Thompson

Respostas:

110

Estranhamente, a história da linguagem de programação no estilo C não começa com C.

Dennis Ritchie explica bem os desafios do nascimento de C neste artigo .

Ao lê-lo, torna-se óbvio que C herdou parte de seu design de linguagem do seu BCPL predecessor , e especialmente dos operadores. A seção “Neonatal C” do referido artigo explica como BCPL da &e |foram enriquecidos com dois novos operadores &&e ||. Os motivos foram:

  • era necessária uma prioridade diferente devido ao seu uso em combinação com ==
  • lógica de avaliação diferente: avaliação da esquerda para a direita com curto-circuito (ou seja, quando aestá falsedentro a&&b, bnão é avaliado).

Curiosamente, essa duplicação não cria nenhuma ambiguidade para o leitor: a && bnão será mal interpretada como a(&(&b)). Do ponto de vista da análise, também não há ambiguidade: &bpoderia fazer sentido se bfosse um valor l, mas seria um ponteiro, enquanto o bit a bit &exigiria um operando inteiro, portanto o AND lógico seria a única opção razoável.

BCPL já usado ~para negação bit a bit. Portanto, do ponto de vista da consistência, poderia ter sido duplicado para dar uma~~ para dar seu significado lógico. Infelizmente, isso teria sido extremamente ambíguo, pois ~é um operador unário: ~~btambém pode significar ~(~b)). É por isso que outro símbolo teve que ser escolhido para a negação que faltava.

Christophe
fonte
10
O analisador não pode desambiguar as duas situações; portanto, os designers de idiomas devem fazê-lo.
BobDalgleish 30/09
16
@ Steve: De fato, já existem muitos problemas semelhantes nas linguagens C e C-like. Quando o analisador vê (t)+1que é uma adição (t)e 1ou é uma conversão +1para digitar t? O design do C ++ teve que resolver o problema de como lexar modelos contendo >>corretamente. E assim por diante.
Eric Lippert
6
@ user2357112 Acho que o ponto é aceitável que o tokenizador tome cegamente &&como um único &&token e não como dois &tokens, porque a a & (&b)interpretação não é uma coisa razoável de se escrever, então um humano nunca teria significado isso e ficado surpreso com o compilador tratando como a && b. Considerando que tanto !(!a)e !!asão coisas possíveis para um ser humano para dizer, por isso é uma má idéia para o compilador para resolver a ambiguidade com uma regra de nível tokenization arbitrária.
Ben
18
!!não é apenas possível / razoável escrever, mas o idioma canônico "converter em booleano".
R ..
4
Eu acho que dan04 está se referindo à ambiguidade de --avs -(-a), os quais são válidos sintaticamente, mas têm semânticas diferentes.
Ruslan
49

Não consigo pensar em uma razão pela qual os designers escolheram se desviar do princípio de que único é bit a bit e o dobro é lógico aqui,

Esse não é o princípio em primeiro lugar; uma vez que você percebe isso, faz mais sentido.

A melhor maneira de pensar em &vs &&não é binário e booleano . A melhor maneira é pensar neles como ansiosos e preguiçosos . O &operador executa os lados esquerdo e direito e calcula o resultado. o&& operador executa o lado esquerdo e, em seguida, executa o lado direito somente se necessário para calcular o resultado.

Além disso, em vez de pensar em "binário" e "booleano", pense no que realmente está acontecendo. A versão "binária" está apenas executando a operação booleana em uma matriz de booleanos que foi compactada em uma palavra .

Então, vamos montar isso. Faz algum sentido fazer uma operação lenta em uma matriz de booleanos ? Não, porque não há "lado esquerdo" para verificar primeiro. Existem 32 "lados esquerdos" para verificar primeiro. Portanto, restringimos as operações preguiçosas a um único booleano, e é daí que a sua intuição de que uma delas é "binária" e outra é "booleana", mas isso é uma consequência do design, não do design em si!

E quando você pensa dessa maneira, fica claro por que não existe !!e não ^^. Nenhum desses operadores tem a propriedade que você pode pular analisando um dos operandos; não há "preguiçoso" notou xor.

Outros idiomas tornam isso mais claro; algumas línguas costumam andsignificar "ansioso e", mas and alsosignifica "preguiçoso e", por exemplo. E outros idiomas também deixam mais claro que &e &&não são "binários" e "Booleanos"; em C #, por exemplo, ambas as versões podem usar os booleanos como operandos.

Eric Lippert
fonte
2
Obrigado. Este é o verdadeiro abridor de olhos para mim. Pena que não posso aceitar duas respostas.
Martin Maat
11
Não acho que seja uma boa maneira de pensar &e &&. Embora a ânsia seja uma das diferenças entre &e &&, &comporta-se de maneira completamente diferente de uma versão ansiosa de &&, principalmente em idiomas onde &&suporta tipos diferentes de um tipo booleano dedicado.
user2357112 suporta Monica
14
Por exemplo, em C e C ++, 1 & 2tem um resultado completamente diferente de 1 && 2.
user2357112 suporta Monica
7
@ZizyArcher: Como observei no comentário acima, a decisão de omitir um booltipo em C tem efeitos indiretos . Precisamos de ambos !e ~porque um significa "tratar um int como um único booleano" e um significa "tratar um int como uma matriz compactada de booleanos". Se você tiver tipos bool e int separados, poderá ter apenas um operador, o que, na minha opinião, teria sido o melhor design, mas estamos quase 50 anos atrasados ​​nesse. C # preserva esse design para familiaridade.
Eric Lippert
3
@ Steve: Se a resposta parece absurda, eu fiz um argumento mal expresso em algum lugar, e não devemos confiar em um argumento da autoridade. Você pode dizer mais sobre o que parece absurdo?
Eric Lippert
21

TL; DR

C herdou os operadores !e ~de outro idioma. Ambos &&e ||foram adicionados anos depois por uma pessoa diferente.

Resposta longa

Historicamente, C se desenvolveu a partir dos idiomas iniciais B, que eram baseados no BCPL, que era baseado no CPL, que era baseado no Algol.

Algol , o bisavô de C ++, Java e C #, definiu true e false de uma maneira que pareceu intuitiva aos programadores: “valores de verdade que, considerados como um número binário (true correspondente a 1 e false a 0), são o mesmo que o valor integral intrínseco ”. No entanto, uma desvantagem disso é que lógica e bit a bit não podem ser a mesma operação: em qualquer computador moderno, ~0é igual a -1 em vez de 1 e ~1é igual a -2 em vez de 0. (Mesmo em um mainframe de sessenta anos, onde ~0representa - 0 ou INT_MIN, ~0 != 1em todas as CPUs já fabricadas, e o padrão da linguagem C exige isso há muitos anos, enquanto a maioria das linguagens filhas nem se dá ao trabalho de suportar sinal e magnitude ou complemento de alguém.)

Algol contornou isso, tendo diferentes modos e interpretando operadores de maneira diferente nos modos booleano e integral. Ou seja, uma operação bit a bit era uma em tipos inteiros e uma operação lógica era uma em tipos booleanos.

O BCPL tinha um tipo booleano separado, mas um único notoperador , tanto para bits quanto lógicos, não. A maneira como esse precursor de C fez esse trabalho foi:

O Rvalue de true é um padrão de bits inteiramente composto por uns; o valor de false é zero.

Observe que true = ~ false

(Você observará que o termo rvalue evoluiu para significar algo completamente diferente nas linguagens da família C. Nós hoje chamaríamos isso de "a representação do objeto" em C.)

Essa definição permitiria lógico e bit a bit não usar a mesma instrução de linguagem de máquina. Se C seguisse esse caminho, diriam os arquivos de cabeçalho em todo o mundo #define TRUE -1.

Mas a linguagem de programação B era de tipo fraco e não possuía tipos de pontos booleanos ou mesmo de ponto flutuante. Tudo era equivalente intem seu sucessor, C. Isso tornou uma boa idéia para a linguagem definir o que aconteceu quando um programa usava um valor diferente de verdadeiro ou falso como valor lógico. Ele primeiro definiu uma expressão de verdade como "diferente de zero". Isso foi eficiente nos minicomputadores em que era executado, que tinham um sinalizador de CPU zero.

Havia, na época, uma alternativa: as mesmas CPUs também tinham uma flag negativa, e o valor de verdade do BCPL era -1; portanto, B poderia ter definido todos os números negativos como verdade e todos os números não negativos como falsidade. (Existe um remanescente dessa abordagem: o UNIX, desenvolvido pelas mesmas pessoas ao mesmo tempo, define todos os códigos de erro como números inteiros negativos. Muitas chamadas do sistema retornam um dos vários valores negativos diferentes na falha.) Portanto, seja grato: ele Poderia ter sido pior!

Mas definir TRUEcomo 1e FALSEcomo 0em B significava que a identidade true = ~ falsenão era mais válida, e abandonara a forte digitação que permitia a Algol desambiguar entre expressões bit a bit e expressões lógicas. Isso exigiu um novo operador lógico-não, e os designers escolheram !, possivelmente porque já não era igual a !=, que parece uma barra vertical através de um sinal de igual. Eles não seguiram a mesma convenção &&ou ||porque ainda não existia.

Indiscutivelmente, eles deveriam ter: o &operador em B está quebrado como projetado. Em B e C, 1 & 2 == FALSEembora 1e 2são ambos valores truthy, e não há nenhuma maneira intuitiva para expressar a operação lógica em B. Esse foi um erro C tentou retificar em parte, adicionando &&e ||, mas a principal preocupação na época era a finalmente, faça um curto-circuito para funcionar e torne os programas mais rápidos. A prova disso é que não existe ^^: 1 ^ 2é um valor verdadeiro, embora ambos os operandos sejam verdadeiros, mas não pode se beneficiar de um curto-circuito.

Davislor
fonte
4
+1. Eu acho que essa é uma boa excursão guiada pela evolução desses operadores.
Steve
BTW, sinal / magnitude e as máquinas de complemento também precisam de negação bit a bit versus lógica separada, mesmo se a entrada já estiver booleana. ~0(todos os bits definidos) é o zero negativo do complemento de uma pessoa (ou uma representação de interceptação). Sinal / magnitude ~0é um número negativo com magnitude máxima.
Peter Cordes em
@PeterCordes Você está absolutamente certo. Eu estava apenas focando nas máquinas com dois complementos, porque elas são muito mais importantes. Talvez valha uma nota de rodapé.
Davislor
Eu acho que meu comentário é suficiente, mas sim, talvez um parêntese (não funcione para o complemento de 1 ou sinal / magnitude) seria uma boa edição.
Peter Cordes em