Almost Always Unsigned

gavide2 pts0 comments

Almost Always Unsigned | Almost Always Unsigned

Almost Always Unsigned

Written by Dale Weiler

Twitter

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&rsquo;re mostly incorrect.

The safety argument

The most typical argument against the use of unsigned integers is that it&rsquo;s<br>more error prone since it&rsquo;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&rsquo;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&rsquo;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&rsquo;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 &ldquo;pathological&rdquo;. Not only is this argument incorrect,<br>it&rsquo;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&rsquo;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&rsquo;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 &ldquo;pathological&rdquo; 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&rsquo;s<br>often the case to want to express that as:

delta = x - y;

Although most of the time the sign isn&rsquo;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...

unsigned rsquo signed value size always

Related Articles