¿Por qué el operador NOT lógico en lenguajes de estilo C es "!" Y no "~~"?

40

Para los operadores binarios tenemos operadores tanto a nivel de bit como lógicos:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

Sin embargo, NOT (un operador unario) se comporta de manera diferente. Hay ~ para bitwise y! por lógico.

Reconozco que NOT es una operación unitaria en oposición a AND y OR, pero no puedo pensar en una razón por la cual los diseñadores optaron por desviarse del principio de que single es bit a bit y double es lógico aquí, y en su lugar optaron por un personaje diferente. Supongo que podría leerlo mal, como una operación de doble bit que siempre devolvería el valor del operando. Pero eso no me parece un problema real.

¿Hay alguna razón por la que me estoy perdiendo?

Martin Maat
fuente
77
Porque si !! significaba lógico no, ¿cómo convertiría 42 en 1? :)
candied_orange
9
¿ ~~Entonces no habría sido más consistente para el NOT lógico, si sigues el patrón de que el operador lógico es una duplicación del operador bit a bit?
Bart van Ingen Schenau
9
Primero, si fuera por coherencia, habría sido ~ y ~~ La duplicación de y / o está asociada al cortocircuito; y el lógico no no tiene un corto circuito.
Christophe
3
Sospecho que la razón de diseño subyacente es la claridad visual y la distinción, en los casos de uso típicos. Los operadores binarios (es decir, de dos operandos) están infijidos (y tienden a estar separados por espacios), mientras que los operadores unarios son prefijos (y tienden a no estar espaciados).
Steve
77
Como algunos comentarios ya han aludido (y para aquellos que no quieren seguir este enlace , !!fooes un idioma no común (¿no común?). Normaliza un argumento cero o distinto de cero para 0o 1.
Keith Thompson

Respuestas:

110

Curiosamente, la historia del lenguaje de programación estilo C no comienza con C.

Dennis Ritchie explica bien los desafíos del nacimiento de C en este artículo .

Al leerlo, resulta obvio que C heredó una parte de su diseño de lenguaje de su predecesor BCPL , y especialmente de los operadores. La sección "Neonatal C" del artículo mencionado explica cómo BCPL &y |se enriquecieron con dos nuevos operadores &&y ||. Las razones fueron:

  • se requería una prioridad diferente debido a su uso en combinación con ==
  • lógica de evaluación diferente: evaluación de izquierda a derecha con cortocircuito (es decir, cuando aestá falsedentro a&&b, bno se evalúa).

Curiosamente, esta duplicación no crea ninguna ambigüedad para el lector: a && bno se interpretará mal como a(&(&b)). Desde el punto de vista del análisis, tampoco hay ambigüedad: &bpodría tener sentido si bfuera un valor l, pero sería un puntero, mientras que el bit a bit &requeriría un operando entero, por lo que el lógico AND sería la única opción razonable.

BCPL ya se usó ~para la negación bit a bit Entonces, desde el punto de vista de la coherencia, podría haberse duplicado para dar un ~~significado lógico. Desafortunadamente, esto habría sido extremadamente ambiguo ya que ~es un operador unario: ~~btambién podría significar ~(~b)). Es por eso que se tuvo que elegir otro símbolo para la negación faltante.

Christophe
fuente
10
El analizador no puede desambiguar las dos situaciones, por lo tanto, los diseñadores de idiomas deben hacerlo.
BobDalgleish
16
@Steve: De hecho, ya hay muchos problemas similares en los lenguajes C y C-like. Cuando el intérprete ve (t)+1es que una adición de (t)y 1o es un elenco de +1tipo t? El diseño de C ++ tuvo que resolver el problema de cómo lex las plantillas que contienen >>correctamente. Y así.
Eric Lippert
66
@ user2357112 Creo que el punto es que está bien que el tokenizador lo tome ciegamente &&como un &&token único y no como dos &tokens, porque la a & (&b)interpretación no es algo razonable de escribir, por lo que un humano nunca hubiera querido decir eso y se hubiera sorprendido por el compilador lo trata como a && b. Mientras tanto !(!a), y !!ason cosas posibles para un ser humano que significa, por lo que es una mala idea para el compilador para resolver la ambigüedad con una regla de nivel tokenización arbitraria.
Ben
18
!!no solo es posible / razonable de escribir, sino el modismo canónico de "convertir a booleano".
R ..
44
Creo que dan04 se refiere a la ambigüedad de --avs -(-a), los cuales son válidos sintácticamente pero tienen una semántica diferente.
Ruslan
49

No puedo pensar en una razón por la cual los diseñadores decidieron desviarse del principio de que solo es bit a bit y doble es lógico aquí,

Ese no es el principio en primer lugar; una vez que te das cuenta de eso, tiene más sentido.

La mejor manera de pensar en &vs &&no es binaria y booleana . La mejor manera es pensar en ellos como ansiosos y perezosos . El &operador ejecuta el lado izquierdo y derecho y luego calcula el resultado. El &&operador ejecuta el lado izquierdo, y luego ejecuta el lado derecho solo si es necesario para calcular el resultado.

Además, en lugar de pensar en "binario" y "booleano", piense en lo que realmente está sucediendo. La versión "binaria" solo está haciendo la operación booleana en una matriz de booleanos que se ha empaquetado en una palabra .

Así que vamos a armarlo. ¿Tiene sentido hacer una operación perezosa en una matriz de booleanos ? No, porque no hay un "lado izquierdo" para verificar primero. Hay 32 "lados izquierdos" para verificar primero. Por lo tanto, restringimos las operaciones perezosas a un solo booleano, y de ahí proviene su intuición de que uno de ellos es "binario" y el otro es "booleano", pero eso es una consecuencia del diseño, ¡no del diseño en sí!

Y cuando lo piensas así, queda claro por qué no hay !!y no ^^. Ninguno de esos operadores tiene la propiedad de omitir el análisis de uno de los operandos; no hay "perezoso" noto xor.

Otros idiomas hacen esto más claro; algunos idiomas suelen andsignificar "ansioso y" pero and alsosignifican "perezoso y", por ejemplo. Y otros lenguajes también aclaran eso &y &&no son "binarios" y "booleanos"; en C #, por ejemplo, ambas versiones pueden tomar booleanos como operandos.

Eric Lippert
fuente
2
Gracias. Esta es la verdadera revelación para mí. Lástima que no puedo aceptar dos respuestas.
Martin Maat
11
No creo que esta sea una buena manera de pensar &y &&. Si bien el entusiasmo es una de las diferencias entre &y &&, se &comporta de manera completamente diferente a una versión ansiosa de &&, particularmente en idiomas donde &&admite tipos distintos de un tipo booleano dedicado.
user2357112 es compatible con Monica el
14
Por ejemplo, en C y C ++, 1 & 2tiene un resultado completamente diferente de 1 && 2.
user2357112 es compatible con Monica el
77
@ZizyArcher: Como señalé en el comentario anterior, la decisión de omitir un booltipo en C tiene efectos colaterales. Necesitamos ambos !y ~porque uno significa "tratar un int como un único booleano" y uno significa "tratar un int como una matriz empaquetada de booleanos". Si tiene tipos bool e int separados, entonces puede tener un solo operador, que en mi opinión habría sido el mejor diseño, pero estamos casi 50 años tarde en ese. C # conserva este diseño por familiaridad.
Eric Lippert
3
@Steve: Si la respuesta parece absurda, he hecho un argumento mal expresado en alguna parte, y no debemos confiar en un argumento de la autoridad. ¿Puedes decir más sobre lo que parece absurdo?
Eric Lippert
21

TL; DR

C heredó los operadores !y ~de otro idioma. Ambos &&y ||fueron agregados años después por una persona diferente.

Respuesta larga

Históricamente, C se desarrolló a partir de los primeros idiomas B, que se basaba en BCPL, que se basaba en CPL, que se basaba en Algol.

Algol , el bisabuelo de C ++, Java y C #, definió verdadero y falso de una manera intuitiva para los programadores: "valores de verdad que, considerados como un número binario (verdadero correspondiente a 1 y falso a 0), es lo mismo que el valor integral intrínseco ". Sin embargo, una desventaja de esto es que la lógica y el bit a bit no pueden ser la misma operación: en cualquier computadora moderna, ~0es igual a -1 en lugar de 1 e ~1igual a -2 en lugar de 0. (Incluso en un mainframe de sesenta años donde ~0representa - 0 o INT_MIN, ~0 != 1en cada CPU que se haya fabricado, y el estándar del lenguaje C lo ha requerido durante muchos años, mientras que la mayoría de sus lenguajes secundarios ni siquiera se molestan en admitir el signo y la magnitud o el complemento de uno.

Algol resolvió esto al tener diferentes modos e interpretar operadores de manera diferente en modo booleano e integral. Es decir, una operación bit a bit era una en tipos enteros, y una operación lógica era una en tipos booleanos.

BCPL tenía un tipo booleano separado, pero un solo notoperador , tanto para bit a bit como lógico. La forma en que este precursor temprano de C hizo ese trabajo fue:

El Rvalue de verdadero es un patrón de bits completamente compuesto de unos; El valor de falso es cero.

Tenga en cuenta que true = ~ false

(Observará que el término rvalue ha evolucionado para significar algo completamente diferente en los lenguajes de la familia C. Hoy lo llamaríamos "la representación del objeto" en C.)

Esta definición permitiría lógica y bit a bit no utilizar la misma instrucción en lenguaje máquina. Si C hubiera seguido esa ruta, los archivos de encabezado en todo el mundo dirían #define TRUE -1.

Pero el lenguaje de programación B era de tipo débil y no tenía tipos booleanos o incluso de coma flotante. Todo era equivalente inten su sucesor, C. Esto hizo que fuera una buena idea que el lenguaje definiera lo que sucedía cuando un programa usaba un valor distinto de verdadero o falso como valor lógico. Primero definió una expresión verdadera como "no es igual a cero". Esto fue eficiente en las minicomputadoras en las que se ejecutaba, que tenían un indicador de CPU cero.

En ese momento, había una alternativa: las mismas CPU también tenían un indicador negativo y el valor de verdad de BCPL era -1, por lo que B podría haber definido todos los números negativos como verdaderos y todos los números no negativos como falsos. (Hay un remanente de este enfoque: UNIX, desarrollado por las mismas personas al mismo tiempo, define todos los códigos de error como enteros negativos. Muchas de sus llamadas al sistema devuelven uno de varios valores negativos diferentes en caso de falla). Así que esté agradecido: ¡podría haber sido peor!

Pero definir TRUEas 1y FALSEas 0en B significaba que la identidad true = ~ falseya no se mantenía, y había dejado caer el tipo fuerte que permitía a Algol desambiguar entre expresiones bit a bit y lógicas. Eso requería un nuevo operador lógico, y los diseñadores eligieron !, posiblemente porque ya no era igual a !=, que se parece a una barra vertical a través de un signo igual. No siguieron la misma convención como &&o ||porque ninguno de los dos existía.

Podría decirse que deberían tener: el &operador en B está roto como se diseñó. En B y en C, 1 & 2 == FALSEa pesar de que 1y 2son ambos valores Truthy, y no hay manera intuitiva para expresar la operación lógica en B. Eso fue un error C trató de rectificar en parte mediante la adición &&y ||, pero la principal preocupación en ese momento era de finalmente ponga en cortocircuito al trabajo y haga que los programas se ejecuten más rápido. La prueba de esto es que no existe ^^: 1 ^ 2es un valor verdadero a pesar de que ambos operandos son verdaderos, pero no puede beneficiarse del cortocircuito.

Davislor
fuente
44
+1. Creo que esta es una visita guiada bastante buena en torno a la evolución de estos operadores.
Steve
Por cierto, las máquinas de signos / magnitud y complemento de uno también necesitan una negación bit a bit separada frente a lógica, incluso si la entrada ya está booleanada. ~0(conjunto de todos los bits) es el complemento negativo a cero (o una representación de trampa). Signo / magnitud ~0es un número negativo con magnitud máxima.
Peter Cordes
@PeterCordes Tienes toda la razón. Me estaba centrando en las máquinas con dos complementos porque son mucho más importantes. Quizás valga la pena una nota al pie.
Davislor
Creo que mi comentario es suficiente, pero sí, tal vez un paréntesis (tampoco funciona para el complemento de 1 o signo / magnitud) sería una buena edición.
Peter Cordes