The Format Specifier That Hid My Own Bug From Me
The exercise existed to prove unsigned integers wrap around correctly. The number on screen said they didn't — and the number on screen turned out to be the only thing in the whole program that was wrong.
This exercise was supposed to just make unsigned wraparound show up on screen, so I'd actually watch it happen instead of only reading about it. I wrote about six lines, ran it, and the output looked like the wraparound hadn't worked at all. Turned out the wraparound was completely fine. It was my printf call that was wrong, the whole time, about a value that had never actually been wrong.
The setup
The setup was about as simple as it gets:
unsigned int x = 0;
printf("x - 1 = %d\n", x - 1);The point was to watch x - 1 underflow past zero and wrap around to UINT_MAX instead of going negative — since unsigned int can't hold a negative number, which is the whole reason it wraps instead of going below zero. I expected 4294967295 on the screen.
I got -1.
What went wrong
It took me a minute to even figure out where the bug actually was. My first instinct wasn't to suspect printf at all — I spent an embarrassingly long stretch wondering whether unsigned subtraction just didn't wrap the way I thought, or whether I'd declared x wrong somehow, or whether I was misremembering what UINT_MAX even was. None of that was it. x - 1 had actually wrapped correctly, to exactly the bit pattern the exercise wanted — thirty-two 1 bits. The bug was one line down, in how I was asking printf to print that pattern.
As far as I understand it, %d just tells printf "read this argument as a signed int," and it doesn't check what the variable was actually declared as — by the time a value reaches a variadic function it's apparently just bits, and printf prints those bits however the format string tells it to. So it read my all-1s pattern as signed, which happens to be -1 in two's complement. Nothing about the value in memory was ever wrong. I'd just told printf to describe it with the wrong specifier, and it did that instantly, no warning, no complaint — which honestly felt worse than if it had just thrown an error.
The fix
printf("x - 1 = %u\n", x - 1);%u reads the same bits as unsigned and prints 4294967295, which is apparently what was sitting in that variable the entire time. (%x would've worked too, I think — same bits, just printed in hex.)
One thing worth noting, since I almost mixed these up myself: elsewhere in that same file there was a completely different, and completely intentional, -Wsign-compare warning — a signed-versus-unsigned comparison left in on purpose to demonstrate that specific pitfall. I actually caught myself about to "fix" it before realizing it was the point of that part of the exercise, not a leftover bug of mine. Telling the deliberate warning apart from the one that was actually mine to fix felt like a small win in the middle of an otherwise humbling exercise.
I think the actual takeaway here is just: check the format specifier against the variable's actual declared type, not against whatever looks tidiest on the line — %u for unsigned, %x when hex is more useful, %d only for values that are genuinely meant to be signed. I'm going to try to actually run that check every time now, though I've probably told myself something like that before, so I'm not going to pretend I'm sure it sticks this time.
What gets me most about this one is that the exercise's entire point was demonstrating unsigned behavior, and a wrong specifier almost erased the exact thing I was supposed to be watching happen. The value in memory was completely, provably correct, and the screen told me it wasn't — and for a solid minute I believed the screen over the thing I'd actually written and understood. That's the part I want to remember out of this, more than the specifier rule itself.
Notes from readers
Comments — via GitHub