I got caught very soon like a child, by a very simple mistake. Let's say I have an array of something, pointers maybe, and in one place I iterate over that array to initialize some objects, and then in some other place I iterate over it backwards to call finalization methods. Array indexes in C/C++ should always be positive, so I wrote:
for (unsigned i = 0; i < count; i++)
array[i]->initialize();
// ...
for (unsigned i = count - 1; i >= 0; i--)
array[i]->finalize();
Oops. A bug. Unsigned ints can never be less than zero, so i >= 0 actually means after the last iteration my poor i will jump to the other end of the world trying to read the 4,294,967,295th element of my array, and will of course fall like a hero. Not to mention that an empty array with count=0 will result in a similar disaster. The shortest correct way of doing backward iteration is:
for (unsigned i = count; i--; )
array[i]->finalize();
This, however, doesn't look terribly elegant, and it is, let's say, not very educational either. I mean, would you show this as the only method of iterating backwards in C to you grandchildren when you retire? No.
The philosophical aspect of this is, when you are very close to the "edge", i.e. the area of zero, and that's where the numbers we deal with in our programs mostly reside, signed ints are safe to cross the edge without catastrophic consequences, because a simple comparison with zero just works in the "natural" way. Something like i >= 0 has been working for centures, since the invention of algebra.
Ok, but that's not all.
In the world of descriptors, handles and memory sizes there is always a special value for no-descriptor, no-handle, or no-location, and that's a typecast'ed -1. For example, there is std::string::npos, which is, I suppose, a cool C++ way of saying just -1, or INVALID_HANDLE_VALUE, which is, in turn, a cool Microsoft way. Apart from that you always have to use a macro or a constant, or a typecast'ed -1 if you are brave enough, there is actually one quite troublesome issue.
Consider you have some unsigned type ufoo_t defined many headers away, and there is also ubar_t defined even farther away, and you kind of have an idea that they bear the same meaning. While juggling with your handles/descriptors or memory sizes you mix values of both types.
Now, one day you decide to compile your project on a 64-bit platform for the first time, and you discover that your invalid memory location (or a handle - whatever it is) is not -1 but 0x00000000FFFFFFFF. Why? Because ufoo_t happens to be declared as unsigned long, whereas ubar_t is unsigned int, and on your 64-bit platform they have different sizes. And because the sign bit is not extended for unsigned ints, it is lost in translation and here you go, your -1 becomes something much less sensible.
I think entities that in some exceptional cases can hold indication of an error are much safer and simpler to declare as signed ints in terms of kicking the value around. One of the most important functions in UNIX, open(), returns -1 on error and there is a whole universe of happily working programs that deal with signed file descriptors and compare the return value with just -1.
So what is it, really, that makes unsigned ints better in any way than signed ints? The upper bounds? I bet you don't even remember them precisely, but I'll remind you that we are talking about 2,147,483,647 vs. 4,294,967,295. What task is it that should be solved on a 32-bit machine and needs numbers exactly between 2 and 4 billion? Because if you deal with numbers below 2b, you are fine with signed ints, and if they are greater than 4b, that's a 64-bit story. In general, if you need billions you simply go for 64-bit to be safe, don't you?
There is one disadvantage of using signed ints that careful programmers would point out, of course, and that's when you check your array index for validity. The condition is:
if (index < 0 || index >= count)
error();
or one comparison more compared to the case with an unsigned index:
if (index >= count)
error();
This is true, but I wouldn't worry about this at all, because index < 0 usually translates to 2 CPU instructions: something like CMP and JMP, where the jump target is just a few instructions away, which means with some optimization techniques available on many modern processors the overhead can be considered in infinite proximity of 0.
All in all, my experiment with unsigned ints was so negative that I'm seriously considering switching back to the old good int, it's just I guess I'm in trouble, because changing my typedef will immediately break a lot of things. I doubt even some Brute Force Programming techniques will help.
Upd: There is always a little dirty trick to make the verification above shorter with signed int, in case you care about microseconds:
if (unsigned(index) >= count)
error();
Updupd: A redditor mentioned Google C++ Style Guide says the same on unsigned ints: Integer types (expand the section to see details).
Long gone the wonderful era of 640 kilobytes of memory and 8 to 12 MHz of clock speed, which "ought to be enough for anybody". Though Mr.Gates never said that, I still think that's a fair amount of memory and good speed for a lot of things. I wouldn't have said that if I hadn't worked in a fully-blown windowed desktop publishing system called Quark XPress on a Macintosh LC, which had, I think, some 2 MB of RAM and ran at 16MHz, back in 1991. Time has passed, the download size of the latest Quark XPress is about half a gig now (as opposed to the prehistoric version that fit a few diskettes at the time), we have gigabytes of memory and we run at staggering gigahertz speeds, and yet the same program with basically same functionality (oh, okay, plus-minus) is slower on a system that's a thousand times more powerful! Or take Acrobat Reader that has grown 15 times in size, has become damning slow and resource-hungry across versions 3.0 through 9.0 while doing same old thing - rendering PostScript programs.
