26

So we had a field issue, and after days of debugging, narrowed down the problem to this particular bit of code, where the processing in a while loop wasn't happening :

// heavily redacted code
// numberA and numberB are both of uint16_t
// Important stuff happens in that while loop

while ( numberA + 1 == numberB )
{
    // some processing
}

This had run fine, until we hit the uint16 limit of 65535. Another bunch of print statements later, we discovered that numberA + 1 had a value of 65536, while numberB wrapped back to 0. This failed the check and no processing was done.

This got me curious, so I put together a quick C program (compiled with GCC 4.9.2) to check this:

#include <stdio.h>
#include <stdint.h>

int main()
{

    uint16_t numberA, numberB;
    numberA = 65535;
    numberB = numberA + 1;

    uint32_t numberC, numberD;
    numberC = 4294967295;
    numberD = numberC + 1;

    printf("numberA = %d\n", numberA + 1);
    printf("numberB = %d\n", numberB);

    printf("numberC = %d\n", numberC + 1);
    printf("numberD = %d\n", numberD);

    return 0;
}

And the result was :

numberA = 65536
numberB = 0
numberC = 0
numberD = 0

So it appears that the result of numberA + 1 was promoted to uint32_t. Is this intended by the C language ? Or is this some compiler / hardware oddity?

peonicles
  • 1,357
  • 2
  • 16
  • 27
  • 3
    FYI, printing `uint32_t` and `uint16_t` using the `%d` format specifier invokes undefined behavior. – The Paramagnetic Croissant Apr 23 '15 at 08:44
  • @TheParamagneticCroissant On the OP's platform, printing an `uint16_t` with `%d` does not cause UB for the same reason that `numberA + 1` is not equal to `numberB`. In all likelihood, the OP's `uint32_t` is defined as `int` and you are correct about undefined behavior for the last two `printf` calls. If `int` were 64-bit wide, the last two `printf` calls would be defined. – Pascal Cuoq Apr 23 '15 at 08:55
  • @MattMcNabb I'm not saying that the OP might have defined `uint32_t` as something or other. I am saying that the OP's platform defines it to either `unsigned int` or something narrower than `unsigned int`. – Pascal Cuoq Apr 23 '15 at 09:00
  • 1
    @PascalCuoq you said "the OP's uint32_t is defined as int" , was that a typo – M.M Apr 23 '15 at 09:00
  • @MattMcNabb Arg, yes, and it is just too late to edit now. – Pascal Cuoq Apr 23 '15 at 09:01
  • Kind of a duplicate of this: http://stackoverflow.com/questions/17312545/type-conversion-unsigned-to-signed-int-char. I think we might need to create a FAQ post regarding implicit type promotions. – Lundin Apr 23 '15 at 09:10
  • 1
    @TheParamagneticCroissant This is very similar if not a duplicate of [Why must a short be converted to an int before arithmetic operations in C and C++?](http://stackoverflow.com/q/24371868/1708801) – Shafik Yaghmour Apr 23 '15 at 09:14
  • I guess the biggest difference (compared to the dups) is that the integer promotion happened *inside* a while loop check where `numberA + 1` was never explicitly assigned to any variable. This made it much harder to find. Beyond that, integer promotion is what it is ... – peonicles Apr 23 '15 at 16:25
  • Thanks to all the contributors! I guess I learnt something new today =) – peonicles Apr 23 '15 at 16:26

4 Answers4

20

So it appears that the result of numberA + 1 was promoted to uint32_t

The operands of the addition were promoted to int before the addition took place, and the result of the addition is of the same type as the effective operands (int).

Indeed, if int is 32-bit wide on your compilation platform (meaning that the type that represents uint16_t has lower “conversion rank” than int), then numberA + 1 is computed as an int addition between 1 and a promoted numberA as part of the integer promotion rules, 6.3.1.1:2 in the C11 standard:

The following may be used in an expression wherever an int or unsigned int may be used: […] An object or expression with an integer type (other than int or unsigned int) whose integer conversion rank is less than or equal to the rank of int and unsigned int.

[…]

If an int can represent all values of the original type […], the value is converted to an int

In your case, unsigned short which is in all likelihood what uint16_t is defined as on your platform, has all its values representable as elements of int, so the unsigned short value numberA gets promoted to int when it occurs in an arithmetic operation.

Pascal Cuoq
  • 75,447
  • 6
  • 144
  • 260
  • To be picky, what you cite is the integer promotion rules, which are indeed part of the usual arithmetic conversions. But "the usual..." are not specified in 6.3.1.1, they are specified in 6.3.1.8. I propose that you change your answer to "...promoted numberA , which is promoted according to the integer promotion rules 6.3.1.1/2:". "The usual arithemtic" rather refers to the balancing between two operands of a binary operator, which doesn't apply in this case. – Lundin Apr 23 '15 at 09:19
  • @Lundin Yes, I (and Matt McNabb) remembered integer promotions as being part of usual arithmetic conversions, but you are right. I removed the reference to them. – Pascal Cuoq Apr 23 '15 at 09:38
  • @mirabilos: The promotion to int only happens if FOO_MAX <= INT_MAX in the first place. uint_32t will not be promoted to int if the latter is 32-bit. – Emil Jeřábek Apr 23 '15 at 11:00
10

For arithmetic operators such as +, the usual arithmetic conversions are applied.

For integers, the first step of those conversions is called the integer promotions, and this promotes any value of type smaller than int to be an int.

The other steps don't apply to your example so I shall omit them for conciseness.

In the expression numberA + 1, the integer promotions are applied. 1 is already an int so it remains unchanged. numberA has type uint16_t which is narrower than int on your system, so numberA gets promoted to int.

The result of adding two ints is another int, and 65535 + 1 gives 65536 since you have 32-bit ints.

So your first printf outputs this result.

In the line:

numberB = numberA + 1;

the above logic still applies to the + operator, this is equivalent to:

numberB = 65536;

Since numberB has an unsigned type, uint16_t specifically, 65536 is reduced (mod 65536) which gives 0.

Note that your last two printf statements cause undefined behaviour; you must use %u for printing unsigned int. To cope with different sizes of int, you can use "%" PRIu32 to get the format specifier for uint32_t.

M.M
  • 130,300
  • 18
  • 171
  • 314
4

When the C language was being developed, it was desirable to minimize the number of kinds of arithmetic compilers had to deal with. Thus, most math operators (e.g. addition) supported only int+int, long+long, and double+double. While the language could have been simplified by omitting int+int (promoting everything to long instead), arithmetic on long values generally takes 2-4 times as much code as arithmetic on int values; since most programs are dominated by arithmetic on int types, that would have been very costly. Promoting float to double, by contrast, will in many cases save code, because it means that only two functions are needed to support float: convert to double, and convert from double. All other floating-point arithmetic operations need only support one floating-point type, and since floating-point math is often done by calling library routines the cost of calling a routine to add two double values is often the same as the cost to call a routine to add two float values.

Unfortunately, the C language became widespread on a variety of platforms before anyone really figured out what 0xFFFF + 1 should mean, and by that time there were already some compilers where the expression yielded 65536 and some where it yielded zero. Consequently, writers of standards have endeavored to write them in a fashion that would allow compilers to keep on doing whatever they were doing, but which was rather unhelpful from the standpoint of anyone hoping to write portable code. Thus, on platforms where int is 32 bits, 0xFFFF+1 will yield 65536, and on platforms where int is 16 bits, it will yield zero. If on some platform int happened to be 17 bits, 0xFFFF+1 would authorize the compiler to negate the laws of time and causality [btw, I don't know if any 17-bit platforms, but there are some 32-bit platforms where uint16_t x=0xFFFF; uint16_t y=x*x; will cause the compiler to garble the behavior of code which precedes it].

supercat
  • 69,493
  • 7
  • 143
  • 184
  • This doesn't actually answer the question. – James Picone Aug 29 '18 at 04:26
  • 1
    @JamesPicone: I supposed I could have added an explicit statement that `uint16_t` is getting promoted because it's smaller than `int`, but that would leave open the question of whether such behavior was "intended by the C language". Assuming the OP is asking about the intentions of the people who designed and documented the language, since the language *itself* isn't sentient, the best way to understand those people's intentions would be to look at that reasons the language was designed as it was. – supercat Aug 29 '18 at 16:24
0

Literal 1 in of int, i.e. in your case int32 type, so operations with int32 and int16 give results of int32.

To have result of numberA + 1 statement as uint16_t try explicit type cast for 1, e.g.: numberA + (uint16_t)1

VolAnd
  • 6,034
  • 3
  • 19
  • 40
  • 2
    Partly right, integer literals are of yup *`int`*, no matter the size of `int`. – Some programmer dude Apr 23 '15 at 08:42
  • 2
    The reason why the addition is between `int` operands is not that `1` has type `int`. The same thing would happen with `numberA + (char)1` or indeed with `numberA + (uint16_t)1`. The recommendation “try explicit type cast …” simply doesn't work. Try it. – Pascal Cuoq Apr 23 '15 at 08:47
  • 1
    `numberA + (uint16_t)1` is indeed nonsense and shows that you don't understand how the type promotions work. Not sure why this received up-votes, as it is incorrect. Also the type of the literal has nothing to do with the result in this specific case. – Lundin Apr 23 '15 at 09:23