Converting the smallest negative long long value to double changes its sign

68 Views Asked by At

I was surprised that converting the smallest negative long long value to double in C changes its sign from negative to positive, so I made a test program.

I'm compiling and running this C program on Linux amd64, linking against glibc:

#include <stdio.h>

int main(int argc, char **argv) {
  (void)argc; (void)argv;
  printf("sizeof(long long)=%d\n", (int)sizeof(long long));
  printf("% .21g is_negative=%d\n", (double)-0x8000000000000000ll, (double)-0x8000000000000000ll < 0.0l);
  printf("% .21g is_negative=%d\n", (double)(-0x8000000000000000ll), (double)(-0x8000000000000000ll) < 0.0l);
  printf("% .21g is_negative=%d\n", (double)(-0x4000000000000000ll * 2), (double)(-0x4000000000000000ll * 2) < 0.0l);
  printf("% .21g\n", (double)(long long)(-0x8000000000000000ll));
  printf("% .21g\n", (double)(-1ll << 63));  /* But this is undefined behavior: warning: shifting a negative signed value is undefined [-Wshift-negative-value]. */
  printf("% .21g\n", -(double)0x8000000000000000ll);
  printf("% .21LgL is_negative=%d\n", (long double)-0x8000000000000000ll, (long double)-0x8000000000000000ll < 0.0l);  /* Surprise: positive! */
  printf("% .21LgL is_negative=%d\n", (long double)(-0x8000000000000000ll), (long double)(-0x8000000000000000ll) < 0.0l);
  printf("% .21LgL is_negative=%d\n", (long double)(-0x4000000000000000ll * 2), (long double)(-0x4000000000000000ll * 2) < 0.0l);
  printf("% .21LgL\n", (long double)(long long)(-0x8000000000000000ll));
  printf("% .21LgL\n", (long double)(-1ll << 63));  /* But this is undefined behavior: warning: shifting a negative signed value is undefined [-Wshift-negative-value]. */
  printf("% .21LgL\n", -(long double)0x8000000000000000ll);
  printf("% lldll\n", -1ll << 63);
  printf("% lldll\n", -0x4000000000000000ll * 2);
  printf("% lldll\n", 0x8000000000000000ll);
  printf("% lldll\n", -0x8000000000000000ll);
  return 0;
}

I get this output:

sizeof(long long)=8
 9223372036854775808 is_negative=0
 9223372036854775808 is_negative=0
-9223372036854775808 is_negative=1
-9223372036854775808
-9223372036854775808
-9223372036854775808
 9223372036854775808L is_negative=0
 9223372036854775808L is_negative=0
-9223372036854775808L is_negative=1
-9223372036854775808L
-9223372036854775808L
-9223372036854775808L
-9223372036854775808ll
-9223372036854775808ll
-9223372036854775808ll
-9223372036854775808ll

The suprising output lines are those not starting with -. On the output I'm expecting all numbers to be negative. Especially that:

  • For (double)(-0x8000000000000000ll) I'm getting a positive output by surprise, even if (double)(long long)(-0x8000000000000000ll) gives a negative output (correctly). What's the difference in here?

  • For (long double)(-0x8000000000000000ll) I'm getting a positive output by surprise, even if (long double)(long long)(-0x8000000000000000ll) gives a negative output (correctly).

Please note that this is not an issue with glibc printf misbehaving on double or long double, that's proven by by the is_negative=0 part of the output.

With GCC on Linux amd64, the double type is the 64-bit IEEE floating point type, with 52 bits for the significand fraction, 11 bits for the exponent and 1 bit for the sign; the long double type is the 80-bit x86 extended precision floating point type, with 64 bits for the significand (1 bit integer, 63 bit fraction), 15 bits for the exponent and 1 bit for the sign.

The integer value 0x8000000000000000 can be represented accurately in both double and long double, because it's a small power of 2. The significand will be 1, the exponent will be 63.

I get the same results with multiple versions of GCC and Clang, both with -m32 (i386) and -m64 (amd64).

1

There are 1 best solutions below

3
pts On

I think I know the answer:

  • The type of 0x8000000000000000ll is unsigned long long (!).
  • The type of -0x8000000000000000ll remains unsigned long long, and the value is actually 0x8000000000000000 (positive).
  • In (double)-0x8000000000000000ll, a positive number gets converted to a double.

Sometimes (but not always, even with -W -Wall) Clang reports a warning (and GCC reports a similar one): comparison of integers of different signs: 'long long' and 'unsigned long long' [-Wsign-compare]

As a proof, the following program prints all 1s:

#include <stdio.h>

int main(int argc, char **argv) {
  (void)argc; (void)argv;
  printf("%d\n", -0x7fffffffffffffffll < 0);
  printf("%d\n", -0x8000000000000000ll > 0);
  printf("%d\n", (long long)-0x8000000000000000ll < 0);
  return 0;
}