5

I recently found a bug on my code that took me a few hours to debug.

the problem was in a function defined as:

unsigned int foo(unsigned int i){
   long int v[]={i-1,i,i+1} ;
       .
       .
       .
 return x ; // evaluated by the function but not essential how for this problem.
}

The definition of v didn't cause any issue on my development machine (ubuntu 12.04 32 bit, g++ compiler), where the unsigned int were implicitly converted to long int and as such the negative values were correctly handled.

On a different machine (ubuntu 12.04 64 bit, g++ compiler) however this operation was not safe. When i=0, v[0] was not set to -1, but to some weird big value (as it often happens when trying to make an unsigned int negative).

I could solve the issue casting the value of i to long int

long int v[]={(long int) i - 1, (long int) i, (long int) i + 1};

and everything worked fine (on both machines).

I can't figure out why the first works fine on a machine and doesn't work on the other.

Can you help me understanding this, so that I can avoid this or other issues in the future?

lucacerone
  • 9,859
  • 13
  • 52
  • 80

3 Answers3

6

For unsigned values, addition/subtraction is well-defined as modulo arithmetic, so 0U-1 will work out to something like std::numeric_limits<unsigned>::max().

When converting from unsigned to signed, if the destination type is large enough to hold all the values of the unsigned value then it simply does a straight data copy into the destination type. If the destination type is not large enough to hold all the unsigned values I believe that it's implementation defined (will try to find standard reference).

So when long is 64-bit (presumably the case on your 64-bit machine) the unsigned fits and is copied straight.

When long is 32-bits on the 32-bit machine, again it most likely just interprets the bit pattern as a signed value which is -1 in this case.

EDIT: The simplest way to avoid these problems is to avoid mixing signed and unsigned types. What does it mean to subtract one from a value whose concept doesn't allow for negative numbers? I'm going to argue that the function parameter should be a signed value in your example.

That said g++ (at least version 4.5) provides a handy -Wsign-conversion that detects this issue in your particular code.

Mark B
  • 95,107
  • 10
  • 109
  • 188
  • thanks for the explanation though I couldn't understand your answer very well. I am not entirely sure, but I remember that long int are large enough to contain unsigned int (at least according to some c++ references that you can find online). Anyway I agree that is good practice not to mix them, but in this case it is necessary for me to do so. I was just curious to know when these machine dependent issues can arise. – lucacerone Sep 18 '12 at 21:26
0

You can also have specialized cast catching all over-flow casts:

template<typename O, typename I>
O architecture_cast(I x) {
/* make sure I is an unsigned type. It  */
static_assert(std::is_unsigned<I>::value, "Input value to architecture_cast has to be unsigned");

assert(x <= static_cast<typename std::make_unsigned<O>::type>( std::numeric_limits<O>::max() ));

return static_cast<O>(x);
}

Using this will catch in debug all of the casts from bigger numbers than the resulting type can accommodate. This includes your case of unsigned int being 0 and subtracted by -1 which results to biggest unsigned int.

Pavel Celba
  • 149
  • 2
  • 8
0

Integer promotion rules in the C++ Standard are inherited from those in the C Standard, which were chosen not to describe how a language should most usefully behave, but rather to offer a behavioral description that was as consistent was practical with the ways many existing implementations had extended earlier dialects of C to add unsigned types.

Things get further complicated by an apparent desire to have the Standard specify behavioral aspects that were thought to be consistent among 100% of existing implementations, without regard for whether some other compatible behavior might be more broadly useful, while avoiding having the Standard impose any behavioral requirements on actions if on some plausible implementations it might be expensive to guarantee any behavior consistent with sequential program execution, but impossible to guarantee any behavior that would actually be useful.

I think it's pretty clear that the Committee wanted to unambiguously specify that long1 = uint1+1; uint2 = long1; and long1 = uint1+1; uint2 = long1; must set uint2 in a manner consistent with wraparound behavior in all cases, and did not want to forbid them from using wraparound behavior when setting long1. Although the Standard could have upheld the first requirement while implementations to promote to long on quiet-wraparound two's-complement platforms where the assignments to uint2 would yield results consistent with using wraparound behavior throughout, doing so would have meant including a rule specifically for quiet-wraparound two's-complement platforms, which is something C89 and--to an even greater extent C99--were exceptionally keen to avoid doing.

supercat
  • 77,689
  • 9
  • 166
  • 211