it-swarm-es.com

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

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 eligieron desviarse del principio de que single es bit a bit y double es lógico aquí, y se fue para un carácter diferente en su lugar. 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?

42
Martin Maat

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 los operadores. La sección "Neonatal C" del artículo mencionado explica cómo los & Y | De BCPL 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 a es false en a&&b, b no se evalúa).

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

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

110
Christophe

No puedo pensar en una razón por la cual los diseñadores eligieron 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 binario y booleano. La mejor manera es pensar en ellos como ansioso y perezoso. Los & operador ejecuta el lado izquierdo y derecho y luego calcula el resultado. Los && 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" es simplemente realizando 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 diferidas a un único booleano, y de ahí proviene su intuición de que uno de ellos es "binario" y uno es "booleano", pero eso es un consecuencia del diseño, no el 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" not o xor.

Otros idiomas hacen esto más claro; algunos idiomas usan and para significar "ansioso y" pero and also significa "perezoso y", por ejemplo. Y otros idiomas también dejan más claro que & y && no son "binarios" y "booleanos"; en C #, por ejemplo, ambas versiones pueden tomar booleanos como operandos.

51
Eric Lippert

TL; DR

C heredó la ! y ~ operadores 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 basó en BCPL, que se basó en CPL, que se basó en ALGOL.

ALGOL , el bisabuelo de C++, Java y C #, definido verdadero y falso de una manera que se sintió intuitivo para los programadores: "valores de verdad que, considerados como un número binario (verdadero correspondiente a 1 y falso a 0), es el mismo que el valor integral intrínseco". Sin embargo, una desventaja de esto es que lógico y bit a bit no puede ser el misma operación: en cualquier computadora moderna, ~0 es igual a -1 en lugar de 1 y ~1 es igual a -2 en lugar de 0. (Incluso en un mainframe de sesenta años donde ~0 representa -0 o INT_MIN, ~0 != 1 en 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 signos y magnitud o un complemento.

ALGOL trabajó alrededor de 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 n único operador not , 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" C ª.)

Esta definición permitiría que lógica y bit a bit no utilicen 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 tenía un tipo débil y no tenía tipos booleanos o incluso de coma flotante. Todo era el equivalente de int en 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 veraz como "no igual a cero". Esto fue eficiente en las minicomputadoras en las que funcionaba, que tenían un indicador de cero de la CPU.

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 definiendo TRUE como 1 y FALSE como 0 en B significaba que la identidad true = ~ false ya no se sostenía, y había dejado caer el tipeo fuerte que permitía a ALGOL desambiguar entre expresiones lógicas y bit a bit 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 que && o || porque ninguno de los dos existía todavía.

Podría decirse que deberían tener: el & el operador en B está roto según lo diseñado. En B y en C, 1 & 2 == FALSE aunque 1 y 2 son ambos valores verdaderos, y no hay una forma intuitiva de expresar la operación lógica en B. Ese fue un error que C intentó rectificar en parte agregando && y ||, pero la principal preocupación en ese momento era finalmente hacer que los cortocircuitos funcionen y hacer que los programas se ejecuten más rápido. La prueba de esto es que no hay ^^: 1 ^ 2 es un valor verdadero aunque sus dos operandos son verdaderos, pero no puede beneficiarse del cortocircuito.

22
Davislor