Compute signed long max value in C using bit shift

4k views Asked by At

Just started learning C yesterday and this is about to drive me crazy on the new year's day... Try to print the different int ranges using bit shift operations. Everything works fine apart from the signed long max/min value. Can't figure out why (1 << 63) - 1 returns -1? But (1 << 64) -1 for unsigned long long works fine...

#include <limits.h>
#include <stdio.h>

void print_range() {
    signed char scmax = (1 << 7) - 1;
    char c = scmax; // char means signed char!
    unsigned char uscmax = (1 << 8) - 1;
    char cmin = -(1 << 7);
    unsigned char ucmin = 0;
    printf("signed char max: %d = %d, unsigned char max: %d = %d\n", scmax, SCHAR_MAX, uscmax, UCHAR_MAX);
    printf("signed char min: %d = %d, unsigned char min: %d\n", cmin, CHAR_MIN, ucmin);

    // signed int
    int imax = (1 << 31) - 1; //(2 << 30) - 1;
    unsigned int uimax = (1 << 32) - 1;
    int imin = -(1 << 31);
    //NOTE: %d is for signed char/short/int, %u is for the unsigned formatter.
    printf("signed int max: %d = %d, unsigned int max: %u = %u\n", imax, INT_MAX, uimax, UINT_MAX);
    printf("signed int min: %d = %d, unsigned int min = %d\n", imin, INT_MIN, 0);

    long long lmax = (1 << 63) - 1L; // WHY DOES THIS NOT WORK???
    unsigned long long ulmax = (1 << 64) - 1;
    long long lmin = -(1 << 63); // NEITHER DOES THIS???
    printf("signed long max: %lld = %lld, unsigned long max: %llu = %llu\n", lmax, LLONG_MAX, ulmax, ULLONG_MAX);
    printf("signed long min: %lld = %lld, unsigned long min: %d\n", lmin, LLONG_MIN, 0);
}
5

There are 5 answers

8
Zbynek Vyskovsky - kvr000 On BEST ANSWER

The expression is evaluated as int as both operands are ints. You need to make them long longs:

((1LL << 63) - 1)
(((long long)1 << 63) -1)

Additionally, many architectures will shift by at most the size of the type -1, so it is shifted by just 31 bits for 63, and by 0 for 32 or 64.

(1<<64)-1) works differently than expected: (1<<64) is 0 due to as explained in previous paragraph. 0-1 is -1 which is converted to long long, which is still -1LL and converted to unsigned long long it results into max unsigned long long (due to 2-complement representation of signed and unsigned numbers in common architectures)

2
chqrlie On

Your code invokes undefined behavior on multiple counts:

  • left shifting 1, an int value, by more than the number of bits in the type minus one invokes undefined behavior ;
  • left shifting a signed integer by any amount such that the resulting value exceeds the range of the type invokes undefined behavior, like all other signed arithmetic overflows.

Computing these maximum values with shifts is inconvenient. If you can assume 2s complement and no padding bits, you can use the bitwise complement to get the maximum unsigned value and shift it once to the right to get the maximum signed value, then negate it and subtract one to get the minimum signed value.

Here is the corrected code:

#include <limits.h>
#include <stdio.h>

int main(void) {
    // char
    unsigned char ucmin = 0;
    unsigned char ucmax = ~ucmin;
    signed char scmax = ucmax >> 1;
    signed char scmin = -scmax - 1;
    char cmax = ((char)(-1)) < 0 ? scmax : ucmax;
    char cmin = ((char)(-1)) < 0 ? scmin : ucmin;
    printf("signed char min: %d = %d, signed char max: %d = %d\n",
           scmin, SCHAR_MIN, scmax, SCHAR_MAX);
    printf("unsigned char min: %d, unsigned char max: %u = %u\n",
           ucmin, ucmax, UCHAR_MAX);
    printf("char min: %d = %d, char max: %d = %d\n",
           cmin, CHAR_MIN, cmax, CHAR_MAX);

    // short
    unsigned short usmin = 0;
    unsigned short usmax = ~usmin;
    signed short smax = usmax >> 1;
    signed short smin = -smax - 1;
    printf("short min: %d = %d, short max: %d = %d\n",
           smin, SHRT_MIN, smax, SHRT_MAX);
    printf("unsigned short min: %d, unsigned sort max: %u = %u\n",
           usmin, usmax, USHRT_MAX);

    // int
    unsigned int umin = 0;
    unsigned int umax = ~umin;
    signed int imax = umax >> 1;
    signed int imin = -imax - 1;
    printf("int min: %d = %d, int max: %d = %d\n",
           imin, INT_MIN, imax, INT_MAX);
    printf("unsigned int min: %u, unsigned int max: %u = %u\n",
           umin, umax, UINT_MAX);

    // long int
    unsigned long ulmin = 0;
    unsigned long ulmax = ~ulmin;
    signed long lmax = ulmax >> 1;
    signed long lmin = -lmax - 1;
    printf("long int min: %ld = %ld, long int max: %ld = %ld\n",
           lmin, LONG_MIN, lmax, LONG_MAX);
    printf("unsigned long int min: %lu, unsigned long int max: %lu = %lu\n",
           ulmin, ulmax, ULONG_MAX);

    // long long int
    unsigned long long ullmin = 0;
    unsigned long long ullmax = ~ullmin;
    signed long long llmax = ullmax >> 1;
    signed long long llmin = -llmax - 1;
    printf("long long int min: %lld = %lld, long long int max: %lld = %lld\n",
           llmin, LLONG_MIN, llmax, LLONG_MAX);
    printf("unsigned long long int min: %llu, unsigned long long int max: %llu = %llu\n",
           ullmin, ullmax, ULLONG_MAX);

    return 0;
}
3
Basile Starynkevitch On

Remember that a programming language is a specification, written in English in some technical report. It is not a software. For C11, see n1570

A literal constant like 1 is not a long, but an int.

To code a literal constant long 1, you need to write 1L (or you might code (long)1....). To code a literal constant unsigned long long 1, you should write 1ULL (or code (unsigned long long)1 which is a constant expression).

A literal constant number fits the "smallest" integral type large enough to represent it. So 1 is an int, and on a 64 bit computer (actually, implementation of C like my Linux/x86-64 one) 10000000000 (that is 1010) is a long (because it does not fit in an int), since on such computers int-s have 32 bits and long-s have 64 bits.

Notice that the size or range of int is not precisely defined by the C99 or C11 standard, and could vary from one implementation to another. You may want to include the <stdint.h> standard header and use types like int32_t ...

So 1 << 63 is an (int)1 left shifted by 63 bits (since left operand is an int the shift is operating on int-s). On my Linux/x86-64, an int has only 32 bits, so that operation is an undefined behavior.

You should be very scared of undefined behavior, see references in this answer. And the sad thing is that occasionally, undefined behavior might happen to work like you desired it (but it still remains UB).

Another way of looking at your code is to be concerned about software portability.


BTW, take the habit to compile with all warnings & debug info, e.g. with gcc -Wall -Wextra -g if using GCC. Sometimes, the compiler is clever enough to warn you in such cases. Then, improve your code to remove all warnings. Later, use the debugger (gdb) to run your code step by step and understand (by querying the program's state in the debugger) what is happening.


In your code you have:

     char c = scmax; // char means signed char!

Sadly, it is much more tricky. You have several variants or dialects of C. In some variants, a char is signed, in others, a char is unsigned. What variant you have is implementation specific (the compiler writer chooses the easiest to implement on some target architecture). With GCC see C dialect options like -fsigned-char and -funsigned-char (which you should almost never use, and when you do use them, be very careful of the consequences; you may need to recompile your entire C standard library).

3
user3629249 On

this line:

long long lmax = (1 << 63) - 1L; // WHY DOES THIS NOT WORK???

does not work because a number is an int unless it is declared otherwise.

AN int is (usually) 32 bits (your compiler should have told you about this with the message:

warning: integer overflow in exression [-Woverflow]

Similarly for the other expression that your having a problem with.

The fact that your compiler did not tell you about this problem means that your compiling without the warnings enabled.

for gcc, at a minimum use:

-Wall -Wextra -pedantic  

I also find these parameters to be very useful:

-Wconversion -std=gnu99
0
Poonus On

(1LL << 63) - 1LL should work, but if you are compiling with flags that detect integer overflow (like -fsanitize=address -fsanitize=undefined), it will give an error. In this case it is better to do:

((1LL << 62) - 1) | (1LL << 62)