Be careful when using unsigned integers at the limit. Especially in if statements with reverse loops in C.
It turns out if you go past the limit of a uint, its value loops back around to the other end!
#include <stdio.h>
#include <stdint.h>
int
main() {
uint32_t low_number = 3;
printf("low_number is %u\n", low_number);
uint32_t lower = low_number - 4;
printf("lower is %u\n", lower);
return 0;
}
$ ./uint-behavior
low_number is 3
lower is 4294967295
The rule specifically, is the value is converted to modulo UINT_MAX + 1. So -1 will convert to 4294967295 for uint32_t
.
I ran into this last week while adding a unit test to a library at work. The test was for seeking from one part of a file to another. The previous engineer had a test that seeked from the end of the file back to the start of the file, entry 0. Something like the very simplified version below.
#include <stdio.h>
#include <stdint.h>
#define NUM_ENTRIES 128
void
seek_test(uint32_t a) {
}
int
main() {
for (uint32_t i = NUM_ENTRIES; i-- > 0;) {
seek_test(i);
}
return 0;
}
I wanted to add a test seeking from the ith entry and then a jump from the ith entry back.
void
seek_test_with_jump(uint32_t a, uint32_t jump) {
}
But I didn’t want want the jump to go past the start of the file, past entry 0. So this is how I wrote my loop:
uint32_t jump_length = 2;
for (uint32_t i = NUM_ENTRIES; i-- > 0;) {
if((i - jump_length) >= 0) {
uint32_t jump = i - jump_length;
seek_test_with_jump(i, jump);
}
}
I didn’t get any compiler warnings when I ran make
, and everything seemed fine. But I was getting errors whenever I ran the tests. Of course, it was because the if statement in the loop would always be true and the seek()
function of the library would return a different result than what my seek_test_with_jump
test would try to compare it to.
My solution was to just use int32_t
, and with that it worked as expected.
#include <stdio.h>
#include <stdint.h>
#define NUM_ENTRIES 128
void
seek_test_with_jump(int32_t a, int32_t next) {
}
int
main() {
int32_t jump_length = 2;
for (int32_t i = NUM_ENTRIES; i-- > 0;) {
if((i - jump_length) >= 0) {
int32_t jump = i - jump_length;
seek_test_with_jump(i, jump);
}
}
return 0;
}
However while writing this blog, I stumbled upon more weird behavior when it comes to uint8_t
.
#include <stdio.h>
#include <stdint.h>
int
main() {
uint8_t jump = 4;
uint8_t low_number = 3;
printf("low_number is %u\n", low_number);
uint8_t lower = low_number - jump;
printf("lower is %u\n", lower);
if(lower >= 0) {
printf("This should print\n");
}
if((low_number - jump) >= 0) {
printf("This should print\n");
}
return 0;
}
$ ./uint-behavior
low_number is 3
lower is 255
This should print
Why isn’t the second “This should print” showing up? At first I thought the 0 was the problem, and I added an explicit conversion to uint8_t
if((low_number - jump) >= (uint8_t)0) {
printf("This should print\n");
}
But I still did not get the second “This should print” to show up.
I got a clue when I printed out what (low_number - jump)
was:
printf("%u\n", (low_number - jump));
$ ./uint-behavior
4294967295
It looked like (low_number - jump)
was being converted to a uint32_t
despite low_number
and jump
being uint8_t
. Sure enough if I force the conversion to a uint8_t
the second “This should print” showed up.
...
if((uint8_t)(low_number - jump) >= 0) {
printf("This should print\n");
}
return 0;
}
$ ./uint-behavior
low_number is 3
lower is 255
This should print
This should print
To further test my hypothesis, I changed all the variables to uint32_t
and removed the explicit conversion in the second if statement.
#include <stdio.h>
#include <stdint.h>
int
main() {
uint32_t jump = 4;
uint32_t low_number = 3;
printf("low_number is %u\n", low_number);
uint32_t lower = low_number - jump;
printf("lower is %u\n", lower);
if(lower >= 0) {
printf("This should print\n");
}
if((low_number - jump) >= 0) {
printf("This should print\n");
}
return 0;
}
$ ./uint-behavior
low_number is 3
lower is 255
This should print
This should print
The second “This should print” showed up again as expected.
All this behavior was probably a big duh to most folks. But as a someone new to C and someone who does not often use unsigned integers, I found it surprising.
The ultimate takeaways for me were:
- When using unsigned integers make sure you don’t go below 0. You will not get compiler warnings.
- Make sure you are comparing the same unsigned integer type,
uint8_t
touint8_t
, oruint32_t
touint32_t
. Otherwise you will get unexpected behavior.