1
$\begingroup$

I have written the following C code which purpose it is to flip an unsigned number in binary representation and give back an unsigned number:

long flippingBits(long n) {
    return (unsigned)(~n);
}

int main(){
    long a = flippingBits(someNumber);
    ...
}

This code does work fine. What about this code?

unsigned long flippingBits(long n) {
    return (~n);
}

int main(){
    unsigned long a = flippingBits(someNumber);
    ...
}

This code indeed prints out negative numbers. WTF. This is beyond my intuition. So is there an (architectural) way, to explain this phenomenon? How does the casting of primitive data types work?

$\endgroup$
2
  • 5
    $\begingroup$ This isn’t Computer Science, but very basic C programming. But since you are here, I’d advise you to google for “C Standard Draft” and use the document you find. $\endgroup$
    – gnasher729
    Commented May 15, 2019 at 15:09
  • 1
    $\begingroup$ There is no print command in the second example. What do you mean by "the code indeed prints out negative numbers"? $\endgroup$
    – JiK
    Commented May 15, 2019 at 19:24

2 Answers 2

2
$\begingroup$

Casting of primitive datatypes works in a very simple way:

Under the hood, values don't have types. Casting does absolutely nothing.

As a simple example, let us consider casting 65 to an ASCII character, thus obtaining the symbol A. What happens under the hood? Absolutely nothing. The only thing that changes is how the program treats the value. When it thinks of it as an integer, it sees 65; when it thinks of it as an ASCII character it sees A.

The CPU does accommodate different datatypes when performing various operations. For example, it has separate instructions for signed and unsigned integer arithmetic, and for floating point arithmetic. Moreover, there are different instruction for different lengths (i.e. 16-bit vs. 32-bit vs. 64-bit for integers). But it is up to the programmer to instruct the CPU which instruction to use. The CPU doesn't keep track of datatypes, which are a higher-level concept.

Modern CPUs do keep a distinction between code and data to some extent, for security purposes. But otherwise the semantics of data is up to the programmer.

Sometimes casting does have an actual effect, and this is during promotion. When casting an integer to a floating point number, or a 32-bit integer to a 64-bit integer, the program calls some instruction which effects this conversion (in the second case, the exact instruction depends on whether the integer is signed or unsigned).

When casting a signed integer to an unsigned integer of the same length (or vice versa), probably nothing happens. This is exactly your case.

$\endgroup$
6
  • 3
    $\begingroup$ "Casting does absolutely nothing" is too broad a statement to lead with. It only does nothing for this particular cast, and only because of the signed and unsigned integer representations we've chosen. Especially for integer->floating or floating->integer casts, the new and old bit patterns in memory could be completely different. If offset binary had somehow become the standard for signed integers, unsigned->signed and signed->unsigned would change the bit pattern too. $\endgroup$ Commented May 15, 2019 at 19:02
  • 1
    $\begingroup$ (We don't have to worry about offset binary in our timeline, and it's forbidden by the C standard, but if things had played out differently, we could have had to deal with integer formats where the signed and unsigned representations of the same nonnegative number don't match.) $\endgroup$ Commented May 15, 2019 at 19:02
  • $\begingroup$ The main point is that datatypes are an artificial construct. Down below it's just bits. Only the program can give them meaning. $\endgroup$ Commented May 15, 2019 at 19:03
  • 1
    $\begingroup$ Plenty of things are misleading or inaccurate in this answer. You're describing a sort of idealized 1970s C on a typical byte-oriented machine, but modern compilers have optimizations that make the semantics of practical-C a lot more subtle, while remaining compliant with the semantics defined by the C standard. “Casting does absolutely nothing” is wrong by any perspective. Between integer types of different ranges, it can apply a modulo operation (or sometimes other effects). When floating point is involved, it can approximate. $\endgroup$ Commented May 15, 2019 at 21:58
  • 1
    $\begingroup$ (cont.) Promotion is a technical term for certain automatic conversions. (A conversion is basically an implicit cast, and a cast is basically an explicit conversion.) There is no casting between 'A' and 65: both are literals of type int. When casting a signed integer to an unsigned integer, the value and (on virtually all implementations) the bit pattern are preserved only if the value is non-negative. $\endgroup$ Commented May 15, 2019 at 22:00
1
$\begingroup$

How does the casting of primitive datatypes works?

The restriction "primitive datatype" is not enough:

Casting from a 16-bit integer to a 32-bit integer works differently than casting between integer types of the same size. And casting between integer and floating-point types is even more complex.

This Code does work fine. What about this code?

Both code examples will work differently on different CPUs, operating systems and possibly using different compilers.

This is because under some operating systems (e.g. 32-bit Windows) long and unsigned data types have the same number of bits; under other operating systems (e.g. 64-bit Windows) long has twice as many bits as unsigned.

And you use the ~ operator (~n) on a signed data type which may cause different results depending on the compiler you use.

This code indeed prints out negative numbers.

So is there an (architectural) way, to explain this phenomenon?

There are two effects:

The first effect is:

If you pass a value of a wrong data type to some function, C will automatically cast the value.

And if you are casting a value that is outside the range of the data type you are casting to, the result will typically not be what you are intuitively expecting.

In the case of two integer data types, normally the "rightmost" bits of the value are copied to the new value.

Let's take (signed char)3478 as an example:

The number 3478 is written as 110110010110 in binary. signed char is an 8 bit data type. The rightmost 8 bits of 1101 10010110 are 10010110. And the value (-106) is stored as 10010110 in a signed char variable.

Therefore (signed char)3478 will result in (-106).

The value a is stored in an unsigned long variable which means that it cannot be negative!

However, you say that you "print out" the value of a. This means you pass the value to some function that prints it to the screen.

Obviously that function expects some signed data type. C will cast the value stored in a to the signed data type and as in the example with the number 3478, the result of casting may be a negative number.

If you use printf you may see a second effect:

If some function (such as printf) has a variable number of parameters, the way the parameters are passed to the function is depending on the data types passed. (The details may also vary from OS to OS.)

You may now pass the wrong data type to the function; for example you use "%d" (signed int) in the printf format string but you pass an unsigned long.

If you do this, the compiler will write the value of a to some data storage (memory or register) which is suitable for unsigned long values before actually calling the function. (For such functions the compiler is not able to find out which data type is really expected by the function, so it has to assume that the function expects the data type you are passing to the function.)

However, printf will look for a value in some data storage suitable for signed int values which might be located somewhere completely different. That data storage does not contain any useful value because the compiler wrote the value of a somewhere else!

$\endgroup$

Not the answer you're looking for? Browse other questions tagged or ask your own question.