back

by raphlinus·3y ago·view on hn ↗
Requiring the shift amount to be unsigned is very sensible.

There are two other issues. One is when the shift amount is greater than or equal to the machine word size, for example 1u32 << 32. Java and Rust (release mode; debug is allowed to panic similarly to other forms of arithmetic overflow) take the position that the shift amount is masked to be modulo the word size, which happens to match Intel and aarch64 assembly instructions but not 32-bit ARM, so (unless the optimizer can prove the range) requires an additional mask instruction. This is the motivation for not explicitly specifying the behavior in C, though I believe a very strong case can be made that it should be implementation defined rather than undefined. In any case, it sounds like you are making the opposite decision as Java, so there's extra logic needed to correctly implement it on Intel and aarch64 but it should work efficiently on 32-bit ARM.

But I was specifically referring to the fact that -1 << 1 is UB in C. It was fixed in C++20 (so the value is specified to be -2, as arithmetic is now defined to be twos complement), but that fix is not in the C23 draft. I consider this perhaps the canonical example of programmer-hostile UB.

1 comments
My second quote above actually cover your cases in the case of "overshift". In the case of overshift, the value is zeroed. Empirically I have found that the vast majority of shift factors can be known at compile time (or at least the valid range of integer), meaning it will compile to 1 instruction rather 2/3.

In naïve C, the shift would look like this:

    (y < 8*sizeof(x)) > (x << y) : 0

In practice, the "select" instruction is never needed. This approach to shifting also places never in that it feels more like 2's complement in that `x << y` is now equivalent to `x * (2*y)`.
That's a reasonable choice and I do agree it has nice properties, for example (a << b) << c is equal to a << (b + c) (unless that latter addition overflows), but it also does put you pretty firmly in the category of "not squeezing out every last cycle like you can in C." Usually that's one of the the things people expect in a tradeoff involving safety.

Regarding "never," I am an adventurous programmer[1], so am fully willing to accept that I am the exception that proves the rule.

On the more general point of UB for arithmetic overflow, I think we're on the same side: this is a pretty broken story in the way C has ended up, and one of the clear motivations for a cleaned up better-than-C language. I'm more interested in your thoughts on data races, where I suspect we have a substantive difference in perspective.

[1]: https://github.com/linebender/kurbo/blob/de52170e084cf658a2a...