Almost Always Unsigned | Almost Always Unsigned
Almost Always Unsigned
Written by Dale Weiler
GitHub
Last updated Saturday, January 1, 2022
The need for signed integer arithmetic is often misplaced as most integers<br>never represent negative values within a program. The indexing of arrays and<br>iteration count of a loop reflects this concept as well. There should be a<br>propensity to use unsigned integers more often than signed, yet despite this,<br>most code incorrectly choses to use signed integers almost exclusively.
Most of the motivation of this article applies to C and C++, but examples for<br>other languages such as Go, Rust and<br>Odin will also be<br>presented in an attempt to establish that this concept applies to all<br>languages, regardless of their choices (for instance C and C++ leave signed<br>integer wrap undefined), but rather is intrinsic to the arithmetic itself.
The arguments against unsigned
There are a lot of arguments against the use of unsigned integers. Let me<br>explain why I think they’re mostly incorrect.
The safety argument
The most typical argument against the use of unsigned integers is that it’s<br>more error prone since it’s far easier for an expression to underflow than it<br>is to overflow. This advice is so common that the official
Google C++ Style Guide<br>outright discourages the use of unsigned types.
We’ll see in the following arguments where these safety issues come from and how<br>to easily avoid them with trivial idioms that are easier to understand than<br>using signed everywhere. We’ll also see that these arguments are incorrect most<br>of the time as they encourage continuing to write and use unsafe code.
The loop in reverse argument
When the counter of a for loop needs to count in reverse and the body of<br>the loop needs to execute when the counter is also zero, most programmers will<br>find unsigned difficult to use because i >= 0 will always evaluate true.
The temptation is to cast the unsigned value to a signed one, e.g:
for (int64_t i = int64_t(size) - 1; i >= 0; i--) {<br>// ...
Of course this is dangerous as it’s a narrowing conversion, with a cast which<br>silences a legitimate warning. In C and C++ it invokes undefined behavior when<br>given specific large values and most certainly is exploitable. Most applications<br>would just crash on inputs >= 0x7ffffffffffffffff. The typical argument is<br>that such a value would be “pathological”. Not only is this argument incorrect,<br>it’s even more dangerous which we will see later.
This danger is one of the supporting arguments behind always using signed<br>integer arithmetic. The argument is incorrect though, because int64_t would<br>never permit a value >= 0x7ffffffffffffffff. It’s only avoiding the issue in<br>that the specific problematic numeric range above that limit is no longer allowed.<br>Tough luck if you needed a value that large and if you followed the sage advice<br>of Google to always used signed and had that large value, well now you have a<br>significantly worse problem, as you now invoked signed overflow unconditionally.<br>Which for languages like C and C++, invoke undefined behavior. While languages<br>like Go and Odin will wrap and have the wrong numeric ranges in the loop<br>as a result of that wrap behavior.
The correct approach here is that unsigned underflow is well-defined in C and<br>C++ and we should be teaching the behavior of wrapping arithmetic as it’s<br>useful in general, but it also makes reverse iteration as easy as forward.
for (size_t i = size - 1; i size; i--) {<br>// ...
The approach here is to begin from size - 1 and count down on each iteration.<br>When the counter reaches zero, the decrement causes the counter to underflow and<br>wrap around to the max possible value of the unsigned type. This value is far<br>larger than size, so the condition i evaluates false and the loop<br>stops.
Languages like Rust chose to make even unsigned underflow a trap representation<br>in Debug builds, but specific features like Range will let you safely achieve<br>the same efficient wrapping behavior on underflow with much cleaner syntax.
for i in (0..size).rev() {<br>// ...
With this approach, no casts are needed, no silent bugs are introduced, and<br>the “pathological” input still works correctly. In fact, this form permits every<br>possible value from [0, 0xffffffffffffffff), covering the entire range.
It should be noted that if size == 0 these loops still work because 0 - 1<br>produces the largest possible value of the unsigned type which is larger than<br>size (still 0) and so the loop never enters.
The difference of two numbers can become negative
When you want to compute the difference (or delta) between two numbers, it’s<br>often the case to want to express that as:
delta = x - y;
Although most of the time the sign isn’t needed so you tend to write and see:
delta = abs(x - y);
The argument is that unsigned is dangerous here because if y > x then you get<br>underflow. The problem with this...