Pitfalls in C and C++: Unsigned Types (2011)
soundsoftware.ac.uk
soundsoftware.ac.uk
> “Unsigned has a bigger range.”
> Only a bit bigger.
So... "only" twice as big.I'm not particularly fond of the trend toward avoiding unsigned integers (in particular the lack of unsigned types in Java and array lengths being signed in C#).
A good programmer should be capable of determining whether to use a signed or unsigned type; if it's not easy to decide, that indicates a design flaw to me.
If you program C like there's no overflow, it doesn't matter much whether you mis-index the next array with 4294967295 or -1. There's no substitute for overflow-vigilance, and particularly not rules like "such-and-such type is bad and such-and-such type is better".
(BTW: an ex-boss of mine claimed that signed ints were evil because because and insisted on using unsigned for everything, but that's a different story, and I'm glad he's an ex-boss)
I think most languages would benefit from unsigned types being the default, and arithmetic overflow being a hard error unless otherwise decorated. Signedness and lenient overflow promote laziness. Array indexes don't make sense as signed, yet most people prefer to iterate arrays with a signed type, e.g most commonly:
for (int i = 0; i < 10; ++i) buf[i] = 0;
Far too many people rely on signed integer overflow working as you expect the underlying machine to handle it - but that's not what the C spec says and not how a compiler handles it either.There are countless security issues I've had to fix related to signed types in what is supposed to be secure code. These would not have occurred if the author was forced to use an unsigned type, and had to consider the extreme limits of the values it can take on. Subtle things such as the addressable limit of memory being naturally unsigned, but buffer sizes being passed as signed, can cause easy exploits, and are stupidly commonplace.
Most commonly you'll actually want to go positive, and have an array going from a[1970] to a[2013] or something similar.
int array[FOO];
int *shifted_array = array + FOO/2;EDIT: just tried it. The following works:
#include <stdio.h>
int main(void)
{
int array[100];
int *shifted_array = array - 1980;
for (int i = 0; i < 100; i++)
array[i] = i;
for (int j = 1980; j < 2014; j++)
printf("shifted_array[%d] = %d\n", j, shifted_array[j]);
return 0;
}More amusing, but less useful outside the IOCCC:
The C subscripting operator performs an basically syntactic transformation, turning a[i] into (*(a + i)). This means that, counter-intuitively (if you're thinking of [] as a lookup) the construct i[a] works too because plus is commutative.
unsigned int i = strlen(s) - 1;
for (; i >= 0; --i) { // BUGBUG
if (s[i] == '.') {
break;
}
}
(Yes, you can make it work, but it is very error-prone when people try). for (unsigned i = strlen(s); i > 0; --i) {
if (s[i - 1] == '.') break;
}
Easy to see that s[i-1] does not underflow the array due to the loop invariant i>0. It's usually easy to convert signed iteration code to unsigned and when I see this, I can tell the author spent the time to consider what happens at the limits of their inputs.Some errors are more subtle and not flagged by compilers, but many are.
Compare this to, say, the "declarations-as-assertions" extension in CMUCL/SBCL: when you declare an unsigned type, underflow conditions will be reported:
* (defun test (x)
(the (integer 0) (1- x))
)
* (test 0)
debugger invoked on a TYPE-ERROR in thread
#<THREAD "main thread" RUNNING {1002B2AF23}>:
The value -1 is not of type UNSIGNED-BYTE.[Edit: Just noticed this got posted, but someone replied. The correct version of this is above.]
(mod (+ x y) N)
The reason I prefer this behavior to C's is that it is more natural. Most of the time when we think of integer arithmetic, we do not assume that it is modulo N; we assume things like " x - 1 < x " will always be true. Forcing unexpected semantics on programmers is a recipe for bugs, and integer underflow/overflow bugs are not uncommon in C programs. #include <iostream>
int main()
{
unsigned I;
// 0 to 4, for loop
for (I = 0; I != 5; ++I) std::cout << I << "\n";
std::cout << "\n";
// 4 to 0, for loop
for (I = 5; I-- != 0;) std::cout << I << "\n";
std::cout << "\n";
// 0 to 4, while loop
I = 0;
while (I != 5)
{
std::cout << I << "\n";
++I;
}
std::cout << "\n";
// 4 to 0, while loop
I = 5;
while (I != 0)
{
--I;
std::cout << I << "\n";
}
std::cout << "\n";
} for (ptrdiff_t i = 0; i != ptrdiff_t(v.size()); ++i)
I think the real answer here is for something like Python's itertools to be in the C++ Standard Library, so that we never need to do anything remotely like the code above. But failing that, it isn't clear to me what the "best practices" version of the above would be.In many other cases, one of the common bugs is unexpected sign extension when a type is promoted to a larger one -- the most common being extension of a signed character to an integer, which can result in some wildly negative values.
Those who have been hit by that frequently will prefer unsigned types.
As with most things, your mileage may vary.
-dB
First, arr[(unsigned)-1] will segfault while arr[-1] will likely cause harder to debug non-local problems.
Second, mod in C and C++ is broken(ish) on signed values: negative % positive returns a negative number. This means in order to use it as an index you need to check the value and add the table size post-mod if you want to wrap an index into an array (as you might for a hash table or ring buffer).
Isn't it equivalent to
* (arr + (unsigned) -1)
, which should be equivalent to * (arr + UINT_MAX)
On typical PC hardware with sizeof(int) == sizeof(char *) that looks like a undefined behavior case to me (maybe barring border conditions; I don't feel like thinking of all edge cases). Most C compilers choose the fastest possible path for undefined behavior cases, so I would expect this to (typically. Again, there will be border cases) be equivalent to arr[-1].(See also http://stackoverflow.com/questions/2578455/pointer-arithmeti...)