Why does the mask expression (1 << bits) - 1 break when bits equals the integer's full width?
answer
- how many distinct shift amounts are legal?
- the count behaves like an index
- hardware masks the count on some targets
- bits equal to width is one past the end
- shifting by the width may mean shifting by zero
basics
~20 sA shift count equal to the operand's width is outside what shifts define: languages variously wrap the count, yield zero, trap, or leave it undefined. Build the all-ones mask without shifting by the full width.
solid answer
~50 sShift counts are only specified for 0 through width-1. At exactly the width, the answer stops being portable: some ecosystems mask the count so a shift by 32 becomes a shift by 0 and the expression returns 0 — an empty mask where you wanted all ones. Others define large counts to produce 0, which by luck gives -1, all ones, and hides the bug. Others call it undefined behaviour, so the optimiser may assume it never happens and the result can change with build flags. Underneath, instruction sets differ the same way, which is why no language gets a free universal answer. A subnet-mask builder written this way passes every test up to a 31-bit prefix and produces nonsense at the full width. Fix it by computing in a wider type and narrowing, or by splitting into two shifts each strictly below the width, and add boundary tests at 0, 1, width-1 and width.
go deeper
Remember that a shift count is valid only from 0 up to one less than the operand's width, and that a full-width shift is not a normal shift. Keep the count in range rather than relying on what your machine happens to do.
Explain the concrete outcomes: a masked count turns a full-width shift into a no-op and leaves an empty mask, while other rules yield zero or leave the result undefined, and show a formulation that avoids the boundary.
Demonstrate the diagnosis: why identical source produces different values on different targets and build settings, why boundary inputs escape fixtures, and which safe formulation plus boundary tests you would require in the fix.
Own the prevention: validate externally supplied shift counts as untrusted input, enable the analysis that flags out-of-range shifts across the fleet's build matrix, and decide whether portability across targets is a property the codebase promises or merely hopes for.
### The rule shifts actually obey A shift by k is specified for k in the range 0 to width-1. Outside that range there is no single agreed answer, and the reason is that there is no single agreed answer *in hardware*: some instruction sets mask the shift count to the low bits of the count register, so a request to shift a 32-bit value by 32 executes as a shift by 0; others feed the count through wider logic and produce 0. A language that promised one behaviour would have to emit a compare-and-branch around every variable shift on the platforms that disagree — a real cost on a very hot instruction — so the specifications instead diverge. ### What each choice does to your mask Write an all-ones mask of width `bits` as `(1 << bits) - 1` and walk it through at `bits == 32` on a 32-bit value: - **Count masked modulo the width.** The shift becomes a shift by 0, so `1 << 32` is 1, and the mask is `1 - 1 == 0`. You get an *empty* mask. Everything you AND with it becomes zero. - **Large counts defined to produce 0.** `1 << 32` is 0, so the mask is `0 - 1`, all ones — accidentally the right answer. The expression is still wrong; it is merely being carried by the platform. - **Undefined behaviour.** The compiler may assume the count is in range. The observed result can differ between optimisation levels, between compilers, and between the debug build where you tested and the release build you shipped. - **Checked at runtime.** The shift traps, and you find out immediately — the best of the four outcomes, and the one least likely to be the platform you are on. So a subnet-mask helper built this way is correct for every prefix that leaves at least one bit unshifted and wrong — or right for the wrong reason — at the boundary. The `/0` and full-width cases are also the ones least likely to appear in a fixture file, which is why this survives review. ### The senior part: why it escaped testing Three properties make this a production bug rather than a caught one. 1. **The failing input is a boundary, not a typical value.** Test tables get written with representative prefixes, and "the whole address space" looks like a degenerate case nobody configures — until an operator does. 2. **The behaviour is a property of the target, not of the source.** Passing tests on your machine is not evidence about the machine that runs it. Cross-compilation, a different architecture in the fleet, or a second runtime consuming the same algorithm can all change the answer with the source unchanged. 3. **Where it is undefined, it is not merely unspecified.** "Undefined" licenses the optimiser to reason as though the case cannot occur, which can delete a surrounding check entirely. That is why "it printed the right number in the debug build" proves nothing. This is the concept where mainstream ecosystems most visibly made different calls: C and C++ leave an out-of-range shift count undefined; Java and JavaScript mask the count to the low bits of the width, so shifting a 32-bit value by 32 returns the value unchanged; Go defines shift counts at or beyond the width to yield zero; Rust treats it as arithmetic overflow, panicking in debug builds and masking in release. Four defensible choices, four different values for one line of source. ### Writing it safely - **Compute in a wider type, then narrow.** Build the mask in a type twice as wide, where `bits` is comfortably in range, and truncate. Simple and obvious. - **Split the shift.** `((1 << (bits - 1)) << 1) - 1` performs two shifts, each strictly below the width, for any `bits` from 1 to the full width. Correct, but it needs a comment or the next reader will "simplify" it back. - **Branch the boundary.** An explicit `if bits == WIDTH` returning an all-ones constant is unglamorous and completely clear. In anything but the hottest loop it is the right answer. - **Derive the mask by complement.** Note that the mirror-image expression, shifting an all-ones value right by `width - bits`, hits the same trap at `bits == 0`. Moving the special case is not removing it. ### Testing and prevention The boundary values worth pinning in tests are 0, 1, width-1 and width — for every width the code is compiled for. Beyond tests: assert or clamp the shift count where it comes from a configuration value or wire input, since an attacker-supplied prefix length is a shift count you did not validate. Sanitiser builds and static analysis flag out-of-range shifts directly, and they are cheap to enable in continuous integration compared to diagnosing one address-space bug in the field. ### The takeaway "Shift by the width gives zero" is a platform observation, not a language guarantee. Treat the shift count like an array index: valid from 0 to width-1, and your responsibility to keep in range.
- How would you write an all-ones mask of width w that is safe when w is the full width?Compute it in a type twice as wide and narrow the result, or split it into two shifts each strictly below the width, or branch out the boundary case explicitly. All three are correct; pick by how hot the code is and how much the reader has to be told. Then pin 0, 1, width-1 and width in tests so a later simplification cannot quietly undo it.
- Why can this pass every test locally and still fail in production?The result is a property of the target, not the source: the same expression can mask the count on one runtime, yield zero on another, and be undefined on a third. Where it is undefined the optimiser may assume the case is unreachable, so the debug build and the release build can disagree with identical source and identical inputs.
- How do you catch this class of defect systematically rather than one instance at a time?Validate or clamp any shift count that comes from configuration or from the wire, since an externally supplied prefix length is an unchecked index. Enable the sanitiser or static-analysis check that flags out-of-range shifts in continuous integration, and make boundary values a standard entry in the test tables for bit-manipulation helpers.
A shift count is like an array index into the bit positions: 0 through width-1 are real slots, and asking for the one just past the end is not a slightly worse answer but a different question every implementation answers its own way.
saying these in an interview costs you the question
- Shifting by the full width just gives zero everywhere
- The compiler would reject an out-of-range shift count
- Undefined behaviour only means an unspecified value
- It passed the tests, so the expression is fine
- Only a negative shift count can cause trouble