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?
~~
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?!!foo
es un idioma no común (¿no común?). Normaliza un argumento cero o distinto de cero para0
o1
.Respuestas:
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:==
a
estáfalse
dentroa&&b
,b
no se evalúa).Curiosamente, esta duplicación no crea ninguna ambigüedad para el lector:
a && b
no se interpretará mal comoa(&(&b))
. Desde el punto de vista del análisis, tampoco hay ambigüedad:&b
podría tener sentido sib
fuera 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:~~b
también podría significar~(~b))
. Es por eso que se tuvo que elegir otro símbolo para la negación faltante.fuente
(t)+1
es que una adición de(t)
y1
o es un elenco de+1
tipot
? El diseño de C ++ tuvo que resolver el problema de cómo lex las plantillas que contienen>>
correctamente. Y así.&&
como un&&
token único y no como dos&
tokens, porque laa & (&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 comoa && b
. Mientras tanto!(!a)
, y!!a
son 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.!!
no solo es posible / razonable de escribir, sino el modismo canónico de "convertir a booleano".--a
vs-(-a)
, los cuales son válidos sintácticamente pero tienen una semántica diferente.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"not
oxor
.Otros idiomas hacen esto más claro; algunos idiomas suelen
and
significar "ansioso y" peroand also
significan "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.fuente
&
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.1 & 2
tiene un resultado completamente diferente de1 && 2
.bool
tipo 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.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,
~0
es igual a -1 en lugar de 1 e~1
igual a -2 en lugar de 0. (Incluso en un mainframe de sesenta años donde~0
representa - 0 oINT_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 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
not
operador , tanto para bit a bit como lógico. La forma en que este precursor temprano de C hizo ese trabajo fue:(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
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 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
TRUE
as1
yFALSE
as0
en B significaba que la identidadtrue = ~ false
ya 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 == FALSE
a pesar de que1
y2
son 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 ^ 2
es un valor verdadero a pesar de que ambos operandos son verdaderos, pero no puede beneficiarse del cortocircuito.fuente
~0
(conjunto de todos los bits) es el complemento negativo a cero (o una representación de trampa). Signo / magnitud~0
es un número negativo con magnitud máxima.