Bit Shifting Horrors
In concept, bit shifting is very simple. Any value in a computer is represented in binary as 0s and 1s, each called a bit. Bit shifting thus is how it reads; it shifts the bits (either left or right).
But things get more complicated when you consider that values in a computer have a defined bit width (the number of bits used to represent the value). So when shifting, what happens to the bits outside of that defined region of memory? And what about for data types that have a dynamic size?
This is where the problems begin for well defining bit shifting. There are many answers that could be given, and unfortunately, many answers across several implementations have been given. Keeping track of which does what and the limitations of it is a nightmare, and the only way to explain why is to dive deep into the ravines of ambiguity.
A Saving Grace in Agreement
Despite what was just mentioned, there are some agreements on how to handle bit shifting, which makes handling the differences more manageable.
Firstly, it's usually the case that no matter what kind of data is being shifted, it's treated as an integer. This will become very handy for understanding a common trend in handling bit shifting.
Secondly, the width of the data being processed is usually respected. When dealing with shifting a value, it makes more sense to not shift everything beyond and completely change everything in memory.
However, this presents a new issue. When shifting left, what fills in the bits on the right? Likewise when shifting right, what fills in the bits on the left? And what happens to the bits shifted out of the region? Thankfully, left bit shifting has a near complete agreement, and right bit shifting has two equally useful agreements.
When shifting left, the bits on the right are filled with 0s, and bits shifted out of the region are discarded. This creates the very nice effect where regardless if your number is unsigned), a << b = a × 2b, except when the result would overflow (1s are pushed beyond the region, losing information until it's all 0s). This overflow side effect is expected and easily controllable.
A Bad Sign
Speaking of negative numbers, this is one of the most subtle issues to arise from bit shifting. Nowadays, almost all signed integers are represented using two's complement, but there are other ways of representing negative integers too. This presents a question: how should bit shifting work on negative numbers? Some implementations completely ignore how the signed numbers work, treating them purely as the bits available, which would be similar to treating it as an unsigned integer. Other implementations will respect the sign and perform a shift while preserving the negative state as necessary.
Sound familiar? These are the logical and arithmetic approaches to shifting! However, a very important consideration is that this might also apply to left bit shifting. Furthermore, there might be only one type of bit shift for each direction, and you don't get a choice on which!
Some implementations do support both. Javascript uses >> for arithmetic right bit shift and >>> for logical right bit shift, but left bit shift only comes in logical flavor, <<. Lua only uses logical for its right shift. C gets really weird, stating that if the result of left-shifting a signed number can't fit in the promoted type (int unless the type is already wider), then it's undefined behavior, and for right-shifting with negative numbers, the result is implementation defined.
In fact, I don't know of a single implementation that supports an arithmetic left shift other than Python. Yes, Python of all languages.
Trying to remember all of these differences in implementation, as well as working around them to ensure they work in all environments—looking at you, C—is a pain, and it's no wonder that bugs arising from related oversights occur often. If you ever see someone's code feverishly casting types in seemingly nonsensical ways, and there's shift operations, there's a good chance that this is why.
Bit Shifting Has Traffic Laws
Here's a thought: what happens if you try to shift by a negative amount? To be clear, I don't mean shifting a negative number; I mean shifting by a negative number. Many implementations consider this to be illegal, but some, including Lua, do. In the latter case, it's usually treated as simply shifting in the other direction, with whatever side effects would normally happen for that direction.
What about trying to shift by more bits than would be reasonable for the bit width? This seems like it would be trivial, since really, what issue is there continuing to shift bits over and over and over? And indeed, there really isn't, but C sure seems to think there is! If you were to, say, shift a 32-bit number by 32 bits, you might be surprised by some undefined behavior! I can only guess that this is a consideration for specific hardware limitations.
Python
Hoo wee...
Okay, first, there is no fixed bit width for integers in Python; instead, it allocates as much memory as is needed to represent the number, whether it's positive or negative, with the only exception being if the number gets far too big, which triggers a failsafe.
This presents an interesting case when shifting negative numbers. Python left-shifts arithmetically. This alone is normal, but remember: there's no fixed bit width. With most other implementations, there's a point where left-shifting is either unsafe or results in a positive or negative number depending on the new most significant bit. In Python, there is no relevant limit on the shifted bits, and since the number of bits allocated is dynamic, the sign is always preserved.
I must make it clear that this is actually a very clever system, avoiding several of the unintuitive side effects that usually happen with bit shifting. However, ironically, its uniqueness makes these results unintuitive for most programmers, since it's different from everything else! Another case to add to the pile.
Sanity Is Floating Away
Floating point numbers are, well, numbers. So what happens when we bit shift those? They have a completely different representation than integers. Should they be converted to integers first? Javascript says yes! C says yes!* Lua says yes! Most implementations agree that performing a bitwise operation on a floating point number is confusing.
However, that doesn't mean it's not correct to do it. When the knowledge is there, using bit shift operations to their fullest potential can yield incredible results. But there are a lot of hoops to just through to even begin to do this properly. Neither Javascript nor Lua support bit shifts on floating point numbers. C sort of has it through pointer casts (float f = 1.0f; int a = *(int*) &f;), but this can be a minefield due to endianness which can order the bytes in unexpected ways.
End of the Line
As mentioned just above, endianness can affect the order of the bytes. Most implementations realized the issues this causes early on, so many strictly consider bit shifting to be an operation on big-endian integers, even if in the memory, they're actually little-endian.
But what if you didn't want that convenience performed automatically? Maybe you're doing byte analysis, so the order being strictly maintained matters a lot. What if you wanted to swap between both endiannesses? How would you even know what your source endianness is? It turns out that there are ways around this, but this demonstrates how even features that are usually convenient can become a serious headache when it's difficult to step around them.
Anti-convenience Convenience
Every implementation attempts to have reasonable convenience and intuitiveness without overengineering. And this is fine! Most programmers won't be touching these low-level quirks.
But for those that do want them, whether it's for research, optimization, challenge, or other, it would be very useful if that convenience didn't ironically serve to become a roadblock. I can't tell you how many times I've written a bit shifting functions by the name of lrs or ars just to do the tasks I wanted.
One could argue that public libraries or modules could help mitigate these problems so that programmers that need them can save time. And while that would help, the syntax for a function is no where near as elegant or readable as a binary operator. Implementors should take note of the desire to break the mold. This isn't an unusual desire either; several CPUs support a variety of bit shifts and other bitwise operations in their ISAs. A notable example is Intel's x86.
And this goes for more than just bit shifting operations as well. There are other bitwise operations that could use some dusting, namely the bit rotations.
In short, when making something convenient, please don't forget to make sure it's convenient to avoid what's been made convenient.
1. ^ Shoutouts to @MtH for having an excellent study of an SVG crime to help me get a footing.
