One possible reason is that bitwise operations on relations would give the wrong results. For example, the code
if ((printf( "hello, %s, it's", higher_power ) == 16) &
(printf( " me, %s\n", luser ) <= 15)) {
would evaluate to false if both conditions are met, if logical operations returned the value of one of their operands, and not execute the body of the loop! And the programmer could not change &
to &&
without potentially short-circuiting the second function call, which has side-effects. When both results are constrained to be either 1 or 0, the above works.
This doesn’t literally use &&
and ||
; I picked a more natural example using comparison relations. The principle is exactly the same. But, in B, the predecessor language to C, there were no ||
or &&
operators, and everyone used &
and |
instead. This worked because false was always 0
and true was always ~0
.
The choice of 1
as TRUE in C, rather than ~0
, also means that the representation of TRUE is the same for every integral type, and can round-trip convert with floating-point, so treating boolean results as int
values could just work. If x == y
returned 0xFFFFU
on a DEC PDP-11 when the types of X and Y were unsigned int
, but 0xFFFFFFFFUL
when the types were unsigned long int
, suddenly any subexpression that widened the result of a 16-bit unsigned boolean true to unsigned long int
width would have produced 0x0000FFFFUL
and caused these logical operations to fail on the upper word. (This had not been an issue for B, as it was an untyped language.) With no special Boolean type, automatic conversion to and from a float
would have been an issue as well. Later, although we know this was unfortunately not something on their minds at the time, the default type promotion rules would have caused even more bugs in the 16-to-32-bit transition. Migrating this code to a 32-bit VAX would have caused all the boolean operations on int
values to suddenly produce 0xFFFFFFFF
, not 0xFFFF
.
The choice of 1 as the value of Boolean true also follows the precedent of Algol, a trendy language in the early ’70s.
The above is my own speculation. Dennis Ritchie would later write about the creation of separate logical operators: What he had to say about it in his own words:
Rapid changes continued after the language had been named, for example the introduction of the && and || operators. In BCPL and B, the evaluation of expressions depends on context: within if and other conditional statements that compare an expression's value with zero, these languages place a special interpretation on the and (&) and or (|) operators. In ordinary contexts, they operate bitwise, but in the B statement
if (e1 & e2) ...
the compiler must evaluate e1 and if it is non-zero, evaluate e2, and if it too is non-zero, elaborate the statement dependent on the if. The requirement descends recursively on & and | operators within e1 and e2. The short-circuit semantics of the Boolean operators in such `truth-value' context seemed desirable, but the overloading of the operators was difficult to explain and use. At the suggestion of Alan Snyder, I introduced the && and || operators to make the mechanism more explicit.
Their tardy introduction explains an infelicity of C's precedence rules. In B one writes
if (a==b & c) ...
to check whether a equals b and c is non-zero; in such a conditional expression it is better that & have lower precedence than ==. In converting from B to C, one wants to replace & by && in such a statement; to make the conversion less painful, we decided to keep the precedence of the & operator the same relative to ==, and merely split the precedence of && slightly from &. Today, it seems that it would have been preferable to move the relative precedences of & and ==, and thereby simplify a common C idiom: to test a masked value against another value, one must write
if ((a&mask) == b) ...
where the inner parentheses are required but easily forgotten.
In Section 2.6 of The C Programming Language, Kernighan and Ritchie specify:
By definition, the numeric value of a relational or logical expression is 1 if the relation is true, and 0 if the relation is false.
Since then, C and all of its successor languages have been boxed in by backward compatibility.
Postscript
Since one commenter requested an example of code involving literal &&
and ||
that breaks when a boolean sub-expression either takes the value of its truthy argument, as you ask about, or promotes from 0xFFFFU
to 0x0000FFFFUL
, as it would have if Ritchie et al. had represented TRUE as in B and given C the same implicit-promotion rules, here is an arbitrary one.
(function_returning_unsigned_int() && function_returning_signed_int()) ^
(unsigned_long_value || function_call()) /* 0x0000FFFFUL ^ 0xFFFFFFFFUL is truthy. */
I make no claim about how likely it is to occur, but at the time, Ritchie, Kernighan and Thompson had a large codebase that always used bitwise operators on logical expressions.
0
means failure or just the usual meaning of0
. You could also have types which aren't "truthy" that can be directly used with optional. You can also have "optional optional" things (which could be an intermediate result, where there are two different kinds of failure that can happen which we want to distinguish) $\endgroup$3 + 4.0
is also valid, despite the operands having different types. This is solved by definining type conversion rules. And this does not explain whyint x = 0 || 3
would result inx
being 1 instead of 3, even thoughx
,0
and3
are all integers. $\endgroup$