Search code examples
c++integer-arithmeticunsigned-char

Unexpected behaviour on underflow of unsigned char


The following program:

#include <iostream>
#include <string>

int main ()
{
    unsigned char result1 {0};
    unsigned char result2 {0};

    result1 = (result1 - 1) % 8;

    result2 = result2 - 1;
    result2 = result2 % 8;

    std::cout << "result1 is " << std::to_string (result1) << '\n';
    std::cout << "result2 is " << std::to_string (result2) << '\n';
}

results in the following output:

result1 is 255
result2 is 7

Why is it that the calculation of result1 and result2 has different results?

I tried several compilers but they all produce the same result so it must be something I don't understand.


Solution

  • Arithmetic in C++ is never done in a type with rank lower than int.

    If you apply - or % to an unsigned char value, which has rank lower than int, it will first undergo integer promotion, meaning it will be converted to an int with the same value (assuming int can hold all values of unsigned char, which is practically always the case).

    The literals 1 and 8 are also of type int, so that then the operations will be performed in the common type int, also with an int result. Notably, this means all arithmetic in your example happens with signed values.

    Only when you assign back to the unsigned char variable is the signed int value converted back to an unsigned unsigned char value.

    So both result1 - 1 and result2 - 1 are ints with value -1.

    The way % treats negative values in C++, (result1 - 1) % 8 is then also an int with value -1.

    Conversion of -1 to unsigned char results in 255 if the width of (unsigned) char is 8 because such conversion is specified to keep the value congruent 2^n where n is the width of the target type.

    However, conversion of result2 - 1 to unsigned char to store in results2 also results in 255, so in result2 % 8 the operation is 255 % 8 (in int) which gives in the int result 7, which converted back to unsigned char is still 7.


    Technically it is also allowed for unsigned char to have the same width as int (although in that case both need to have at least 16 bit width, because that is the minimum width required for int by the standard.)

    In that case, int can't hold all values of unsigned char and integer promotion would promote to unsigned int instead. Then, the usual arithmetic conversions will prefer unsigned int over the int type of the literal operands and so all arithmetic will be done in unsigned int.

    Under these circumstances both of your outputs will give 7. Whether any C++ implementation with these properties actually exists, I don't know, but it is permitted by the standard.