Uri Blass wrote:I do not understand much about hardware but I think that it could be better to have definition for every possibility instead of having undefined cases.
If the fastest implementation of << that works correct for positive is dependent on the compiler then it is possible to have also another symbol to represent the faster operation that does not work for negative.
The point I was trying to get across is more like this: Yes its true that all x86 PCs, and most other modern desktop and server machines, work a certain way. But the C language is used on other kinds of computers too. There are C compilers for 1970's mainframes, for connection machines and for the little embedded processors in your TV or washing machine. C was designed in an era when there were machines with 7-bit or 9-bit bytes, a pointer might have 36 bits in it, etc. It had to accomodate all sorts of weird hardware (and still does to some extent). Nowadays everybody agrees that a byte has 8 bits in it, but even that was not always true around the time C was invented.
The C language is specified in such a way that the compiler implementors can do whatever is fastest for their kind of hardware. So the language doesn't specify something like "function arguments are evaluated in left-to-right order" for example, because for some types of machines, and some programs being compiled for them, there will be a better or faster way. The language tries to give the compiler guys the freedom to do whatever is fastest or smallest or best. The price paid for that, is that all the programmers using C have to remember not to assume that arguments are evaluated in any particular order. So expressions like x++ = ++x + 1 could mean just about anything, because the language does not spell out their One True Meaning(tm).
Whereas a language like Java does try to define the behaviour of everything. That caused a bunch of problems with floating-point math because the Java language specified some things (like the exact precision that was supposed to be used in some situations) that the floating-point hardware on many platforms could not deliver, without using a bunch of extra instructions, like 5x as many. As a result, some modern floating-point hardware has a "Java-compatibility mode" or similar. The Java guys realized their mistake and eventually backed down on the float stuff, nowadays the programmer can choose between "fast-but-not-exactly-the-same-on-all-platforms" floats, and "slow-but-works-the-same-everywhere" floats.
Uri Blass wrote:The reason for the definition that I thought about has nothing to do with hardware and it is simply a trivial mathematical generalization.
It is trivial that every hardware can support translation of << for negative number to >> and the compiler simply can translate
a<<b to
(b>=0)? (a<<b):(a>>(-b));
Uri
Maybe they could put that in the spec. But what if there's some hardware out there that would now need extra instructions? Instead of compiling a shift to one instruction, it might have to compile it to 6 or 10 instructions on this platform, crippling the performance. C tries hard not to do that.
Besides, what do you do if (b) is not a constant? Now the compiler would have to insert extra run-time stuff---a branch, or predicated instructions, or a conditional move, or something. The C language tries to avoid features with a "hidden run-time cost". After all, if (b>=0)? (a<<b):(a>>(-b)) is what you want, you can always put it in a macro or something.
C was invented for systems programming, and took a very pragmatic approach when it came to supporting different hardware and different features and trying to get the best of everything. This is part of what made C so successful, but it does mean that people trying to write
portable code have to be very careful not to fall into any of the pitfalls of the language.
I once worked on a team that had C code targeting over 20 different platforms, spanning every popular CPU architecture and a full set of compilers including MSVC, Intel, GCC and several compilers for crazy embedded platforms. Most of my time was spent fixing the weird portability problems that cropped up on one of the lesser-used targets anytime someone on the team changed code. As in, someone would check in their code changes, and the next day the automated builds or tests would fail on these platforms, and we had to figure out why and then change the code so that it worked on all of them. Portable low-level code is possible but the language does not make it easy for you.