counting up vs counting down
The “traditional” way of writing a for loop looks something like this:
for (size_t i = 0; i < numBurritos; i++)
if (wantBurrito(i))
break;
Start at zero and count up, working through the array. But sometimes you’ll see an alternative version that counts down.
for (size_t i = numBurritos - 1; i >= 0; i--)
if (wantBurrito(i))
break;
There’s a few reasons to do things this way. Maybe the super burritos are at the end of the list, and we’d obviously prefer bigger. Because better.
But there’s a potential bug. If the break
doesn’t trigger, the loop bound of i >= 0
won’t terminate the loop either. Unsigned numbers will only ever be greater than or equal to 0. Now there’s an infinite loop.
Either version of the loop is likely to work in most cases. It will probably pass all the unit tests. No matter whether you prefer steak or al pastor or even vegetarian, you’ll always find a burrito you want and exit the loop. I mean, how can you not want a burrito?
Well, in special circumstances like Taco Tuesday, maybe it’s true you don’t want a burrito. And now you’re the indecisive jerk at the front of the line, holding everybody up as you mindlessly dig through the trash trying to interpret stale avocado shells as part of the menu. (Protip: you don’t want the $2 tequila, even on Taco Tuesday.)
This bug is fairly popular, but there’s ways to avoid it. One can start by trying to coerce a warning out of the compiler, and then looking at the warnings it generates. That will find the problem, but still leaves solving it.
The simplest fix is to use a signed variable. Perhaps int
. But int has its own problems with potential overflow. Especially consider the case of an index into a string (char *
) on a 64 bit platform. size_t is the appropriate type for an index because the limited range of int is not nearly enough. The signed variant, ssize_t, looks funny and it’s hard to get into the habit of using it.
Perhaps rewrite the loop so that it goes forward. Even if you want to work backwards through the array, counting up means the loop will always terminate.
for (size_t i = 0; i < numBurritos; i++) {
size_t idx = numBurritos - i - 1;
if (wantBurrito(idx))
break;
}
Is that better? Is that worse?
We’re trying to do two things at once, creating a for loop and calculating array indices. These activities are so easily combined we may not even realize they can be two separate operations. Splitting them up allows us to focus on each part individually.
There’s conflict here between clarity and cleverness and convention. I’ve found, though, that attempts to move beyond very simple, obviously correct, rote loops increase the error rate drastically. And sometimes the errors are hard to see. I tend to miss the counting down problem because I always write my loops going forward. It’s not that I don’t understand loops that count down, or haven’t seen them before, but the possibility of the loop bound not terminating is rarely on my mind.
Wizened sourcerers may also elect to drop convention entirely and go full clever.
for (size_t i = numBurritos - 1; i != -1; i--)
(That was a joke.)
It has been suggested that one can move the iteration into the test and adjust accordingly.
for (size_t i = numBurritos; i-- > 0; )
if (wantBurrito(i))
break;
I don’t usually see loops like this, so it took me a little extra effort to verify this is correct, but it is. The index looks to be too large, but it’s decremented before use, and we will hit zero inside the loop body.