What’s wrong with this code?

```
int8_t absolute(int8_t x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
```

Seems straightforward enough. Let’s try it with some representative numbers:

```
#include <stdint.h>
#include <stdio.h>
int8_t absolute(int8_t x) {
if (x > 0) {
return x;
} else {
return -x;
}
}
int main() {
int8_t values[5] = {INT8_MIN, INT8_MIN + 1, 0, INT8_MAX - 1, INT8_MAX};
for (int i = 0; i < 5; i++) {
int8_t x = values[i];
printf("abs(%4i) = %4i\n", x, absolute(x));
}
return 0;
}
```

Running this code yields:

```
eudoxia@bullroarer $ gcc cabs.c
eudoxia@bullroarer $ ./a.out
abs(-128) = -128
abs(-127) = 127
abs( 0) = 0
abs( 126) = 126
abs( 127) = 127
```

The very first case is wrong. Why? Because signed integers are asymmetrical
around zero. Note how `INT8_MAX`

is 127, while `INT8_MIN`

is -128. You can think
of it in terms of a number line, with the negative side being larger by one:

More generally: if a signed two’s complement number has *n* bits, the largest
number it can represent is 2^{(n - 1)} - 1, while the most negative
number it can represent is -2^{(n - 1)}

You can think of unary negation as rotating a number around zero on the number
line. Evaluating `-(-127)`

rotates the number and lands on 127:

Evaluating `-(-128)`

rotates the number around zero, but it lands one step beyond
`INT8_MAX`

. Because of overflow, it lands right back on `INT8_MIN`

.

Note that compiling with `-ftrapv`

doesn’t help. Neither GCC nor Clang catch
this. Ada does, though:

```
with Ada.Text_IO;
procedure AdaAbs is
type Signed_Byte is new Integer range -128 .. 127;
function Absolute(X: Signed_Byte) return Signed_Byte is
begin
if X >= 0 then
return X;
else
return -X;
end if;
end Absolute;
type Index is range 1 .. 5;
type Value_Array is array (Index) of Signed_Byte;
Values: Value_Array := (127, 126, 0, -127, -128);
package Signed_Byte_IO is new Ada.Text_IO.Integer_IO (Signed_Byte);
begin
for I in Index loop
declare
X: Signed_Byte := Values(I);
begin
Ada.Text_IO.Put("abs(");
Signed_Byte_IO.Put(X);
Ada.Text_IO.Put(") = ");
Signed_Byte_IO.Put(Absolute(X));
Ada.Text_IO.New_Line;
end;
end loop;
end AdaAbs;
```

Running this yields:

```
eudoxia@bullroarer $ gnatmake adaabs.adb
x86_64-linux-gnu-gnatbind-10 -x adaabs.ali
x86_64-linux-gnu-gnatlink-10 adaabs.ali
eudoxia@bullroarer $ ./adaabs
abs( 127) = 127
abs( 126) = 126
abs( 0) = 0
abs(-127) = 127
abs(-128) =
raised CONSTRAINT_ERROR : adaabs.adb:11 range check failed
```

I only became aware of this issue from an AdaCore case study, probably this, about using SPARK Ada to prove overflow-safety in an implementation of the absolute value function. Here is such an implementation.

This case was a big part of my motivation for having pervasive overflow checking in Austral: the fact that the trivial implementation of the absolute value function – which matches its mathematical definition exactly – is subtly wrong should be humbling, and prove the futility of having programmers mentally track every overflow possibility.

The takeaway: when working with fixed-width integers, test on extremal
values. And don’t trust `-ftrapv`

.