2

Question

I have a program with two versions: one using unsigned integers and the other using unsigned long.

As I need to compute the difference between 2 unsigned numbers, I store it in a long int. An inconsistency appears: the substraction of two unsigned long works well but the substraction of unsigned int "wraps around". Moreover, this does not happen if we store the difference in a standard int.

Here is a minimal example in c++ (but it probably happens in c too):

  unsigned long ul3 = 3;
  unsigned long ul4 = 4;
  unsigned u3 = 3;
  unsigned u4 = 4;
  
  long int li;
  li = ul3 - ul4; cout << li << endl; // case a: prints -1
  li = u3 - u4;   cout << li << endl; // case b: prints 4294967295, which is 2^32-1
  
  int i = u3 - u4;
  i = ul3 - ul4;  cout << i << endl;  // case c: prints -1
  i = u3 - u4;    cout << i << endl;  // case d: prints -1

Do you know what the reason is? And how can I reliably fix it?

A solution is to cast u4 as a long int before substracting it: li = u3 - ((long int) u4);, but it seems like an unreliable fix.

Note that I am aware of the modularity of unsigned numbers (Is unsigned integer subtraction defined behavior?), but it does not seem to explain the above problem.


Short answer

Case b has a combination of types that follows a specific rule, which makes the wrap-around of unsigned substraction persist in the signed variable. The other cases follow different rules.

Answer

The answer follows from the complicated Implicit type promotion rules .

For any operation between two integers (that are not smaller than standard int):

If both operands have the same type, then no further conversion is needed.

In the - operation, the initial unsigned type is kept and wraps around, leading to a value of 2^32-1 (cases b,d) or 2^64-1 (cases a,c).

Otherwise, if the operand that has unsigned integer type has rank greater or equal to the rank of the type of the other operand, then the operand with signed integer type is converted to the type of the operand with unsigned integer type.

Case a: In the = operation, the ranks of types long int and unsigned long are equal, so (ul3-ul4), which is 2^64-1 is stored in li as an unsigned long. When we print, this number is bigger than 2^63-1 (limit of long int), so it is considered as a negative number. Hence the output of -1.

Case d: In the = operation, the rank of type unsigned long is greater than the rank of int so (u3-u4), which is 2^32-1 is stored in i as an unsigned long. When we print, this number is bigger than 2^31-1 (limit of int), so it is considered as a negative number. Hence the output of -1.

Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, then the operand with unsigned integer type is converted to the type of the operand with signed integer type.

Case b: In the = operation, type long int can represent all the values of type unsigned long, so (u3-u4), which is 2^32-1 is converted to long int. Hence the output of 2^32-1.

Otherwise, both operands are converted to the unsigned integer type corresponding to the type of the operand with signed integer type.

Case c: In the = operation, int cannot represent all unsigned long, so (ul3-ul4), which is 2^64-1 is stored in i as an unsigned long. When we print, the same thing happens as in cases a and d. But why does it also work with an int?

Vendec
  • 73
  • 2
  • 8
  • I think it does, thank you. This is so complicated, i may try to write an answer to my question! – Vendec Jan 04 '22 at 18:31
  • On StackOverflow, you should not write an answer inside a question. Instead, you should post an answer to your own question. – prapin Jan 04 '22 at 19:27
  • @prapin the question was already closed when the answer was edited in, so posting a separate answer wasn't possible. I don't think the edit was necessary, but I'm not going to condemn it. – Mark Ransom Jan 05 '22 at 03:45
  • Note that in all your examples there are *two* possible places for type conversions to happen. First is in the inputs to the arithmetic operation. Second is in the assignment to the final variable. When a signed is converted to an unsigned of the same size or vice versa, the most common observed result is that you get the same bit pattern, just interpreted differently. – Mark Ransom Jan 05 '22 at 03:56

0 Answers0