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:

  1. When using unsigned integers make sure you don’t go below 0. You will not get compiler warnings.
  2. Make sure you are comparing the same unsigned integer type, uint8_t to uint8_t, or uint32_t to uint32_t. Otherwise you will get unexpected behavior.