Improving C operators
2015-06-06 Permalink
Over the years C’s expression syntax was frequently praised for its expressiveness. No wander many programming languages inherited parts of the C grammar and its expressions, despite them being considered too cryptic for novices. However, C operator precedence and associativity rules have a bunch of historical dumbness. This would be OK on its own, but those programming languages, including C++, Java and C#, did not bother to fix any of these problems.
Shift operators should have the same precedence as multiplicative operators. They are essentially multiplications by a power of two. For example, it is more likely that
a + 1 << b
was meant to be a + 2b rather than (a + 1) 2b.Bitwise and, xor and or should have precedence higher than that of comparison operators. You never need to do a bitwise and on the result of an equality check, since the later is always a Boolean, so the logical and will do. On the contrary, bit fields are frequently checked like this:
if(a & mask == flagA | flagB) // ...
Here
a & mask
andflagA | flagB
should be evaluated first.Assignment operators associativity should be reversed. Specifically, the assignment operator is alright, its right associativity lets us write expressions like
a = b = 0
to zero out a and b. Assigningb
toa
and then overwritinga
with 0 would make no sense.However, this interpretation is rarely useful for the rest of the assignment operators. It is frequent to add things up. Like:
a = a + b + c
. In C it does not matter that much, but for C++ it is frequently more efficient to update an existing object inplace rather than constructing a temporary. Think of:std::string a = ...; a += b += c;
to mean 'append b and c to a’. I’ve seen use cases for the opposite, but they are extremely rare.
Logical operators should return the last evaluated operand rather than a Boolean. It lets us write code like
FILE *b = fopen(filename0, "rb") || fopen(filename1, "rb");
Good, but how it is going to work in a statically typed language you ask? The following list does not cover all the edge cases, but it does give a general picture of the intended behavior and the possible uses.
- The operands should be converted to a common type by rules similar to those employed by the conditional operator, with the extra stipulation that if those rules do not apply, the resulting type is
bool
.fflush(stdout) || exit(1); // result type -- void if(fflush(stdout) || exit(1)); // error -- cannot convert void to bool
- The conversion to
bool
should happen before the conversion to the common type. - The conversion to
bool
should not happen on an operand that is itself a logical expression. Instead, its bool result must be reused. So that inshared_ptr<X> f(int); shared_ptr<X> s = f(0) || f(1) || f(2);
shared_ptr<X>::operator bool
is called at most once on each of the returned values.
- The operands should be converted to a common type by rules similar to those employed by the conditional operator, with the extra stipulation that if those rules do not apply, the resulting type is