A Small Pitfall of Implicit Type Conversion in C

This issue is roughly as follows. I originally tried to write a statement that produces a mask with the low 8 bits set to 1: uint32_t mask = ~((uint8_t)0);, but found that the computed mask was 0xffffffff, which obviously was not what I expected. After some investigation, I wrote the following comparison program:

:::C++
#include <stdint.h>
#include <iostream>

using namespace std;
int main()
{
  uint8_t z = 0;
  uint32_t x = ~(uint8_t)0;
  uint32_t y = (uint8_t)~0;
  cout << typeid(~(uint8_t)0).name() << endl;
  cout << typeid((uint8_t)~0).name() << endl;
  cout << x << endl;
  cout << y << endl;
}

The program output (macOS, Clang) is:

i
h
4294967295
255

So it is obvious that the subtle difference between these two forms leads to different results. With typeid, we can see that the types of the two expressions are actually different. The latter h clearly indicates uint8_t, while the former is presumably int. So it seems there are rules about implicit type conversions here. I immediately went to check the C99 standard; as expected, the beginning of section 6.5 specifies the following:

C99

And in 6.5.3.3 there is also the following explanation:

C99

Therefore, here the bitwise NOT operator actually applies integer promotion to uint8_t, and the result of the NOT operation is also the integer type -1. When converted to an unsigned integer, this naturally causes all bits to be 1, rather than only the lowest byte being 1 as expected.

Based on the above definition, we can derive another pitfall. Consider the following code:

:::C
#include<stdio.h>
#include<stdint.h>
int main(){
    printf("0x%016llx\n", (uint64_t)~0u);
    printf("0x%016llx\n", (uint64_t)~0);
}

Its output is:

0x00000000ffffffff
0xffffffffffffffff

As you can see, the type of ~0u is uint32_t, so it undergoes zero extension; while the type of ~0 is int32_t, and its value is -1, so the type conversion performs sign extension, producing the expected all-1 mask. Therefore, when using type casts to generate masks, these small pitfalls require special attention, as they can easily lead to unintended results. I think this is also why most code uses shift operations to construct masks.

In short, if the semantics are unclear, go check the relevant spec.

comments powered by Disqus
Published:
2016-11-07
Category:
Tag:
C7