It may
seem like an exceedingly bad thing, but in practice most shift amounts are constants, statically known to the compiler, which makes choosing between single instruction-long sequences "shl eax/rax, <imm8>", "shr eax/rax, <imm8>", and "xor eax, eax" trivial.
But variable shifts also happen sometimes, which brings me to the next point:
> in exchange for UB in cases that never occur in practice.
It does happen, and people depend on what their platform does. That's one of the things that sunk John Regehr's proposal for "Friendly C" [0]:
After publishing the Friendly C Proposal, I spent some time discussing its design with people,
and eventually I came to the depressing conclusion that there’s no way to get a group of C
experts — even if they are knowledgable, intelligent, and otherwise reasonable — to agree on
the Friendly C dialect. There are just too many variations, each with its own set of performance
tradeoffs, for consensus to be possible. [...] Another example is what should be done when a
32-bit integer is shifted by 32 places (this is undefined behavior in C and C++). Stephen Canon
pointed out on twitter that there are many programs typically compiled for ARM that would fail
if this produced something besides 0, and there are also many programs typically compiled for
x86 that would fail when this evaluates to something other than the original value. So what is
the friendly thing to do here?
So. You want a modulo behaviour? Write "expr << (var & 32)" or "expr << (var & 64)", the compiler will recognize this idiom and emit a simple "mov cl, var; shl eax/rax, cl" on x64 and RISC-V, and an additional explicit AND mask on ARMs. You want an overshift? Well, that's the default semantics, and it would emit an simple shift on ARM, and "xor eax, eax; shl edx/rdx, cl; cmp ecx, 32/64; cmovb eax/rax, edx/rdx" on x64 (and something similar on RISC-V). It's your choice!
[0] https://blog.regehr.org/archives/1287