cbitwise-operators

C invert the bits of a 16 bit integer


This code won't simply invert the bits of the 16-bit integer as expected:

#include <stdint.h>
typedef uint16_t U16;

U16 myNum = 0xffff;
~myNum // 0xffff0000, not 0x0000

Because C will promote the 16-bit integer to a 32 bit integer before doing the inversion (see 1, 2).

What are my options for 16-bit bitwise inversion? I can think of:

~myNum & 0xffff
// or 
((U16)~myNum)

But they seem prone to mistakes/misunderstanding. Am I just out of luck if I want a neat 16-bit inversion?


Solution

  • Short story: just cast back to the intended type, (uint16_t)~myNum


    Long story: The problem is indeed that due to implicit promotion, almost all C operators that started on a small integer type such as char/short etc end up using int type instead. This is particularly troublesome in case you have a uint16_t on a 32 bit machine and apply ~, because the operand will get promoted to type int, the result will be of type int and therefore a negative value in this case.

    General best practices with ~ is to always cast the result back to the intended type, in this case (uint16_t)~myNum. Conversion to unsigned is well-defined as per taking the mathematical value modulo 2^n, where n is the number of bits in the unsigned type. Or if you will, simply discard the upper bytes of the int, leaving us with 0x0000.

    Also, even though there are implicit promotions present, the compiler is smart enough to tell that in case of (uint16_t)~myNum there are no unintended side-effects such as change of sign. So it could optimize this expression to get carried out of 16 bit types in the machine code, if that leads to faster execution (certainly the case on 8/16 bit CPUs). Whereas something like if(~myNum > 0) can't get carried out on 16 bits, because the implicit conversion to int caused a change of signedness and gave a negative value.

    Best practice is to never writing code containing or relying on implicit promotion, or pretending that it isn't there. For example myNum = ~myNum; works too but this code could either mean two things: either the programmer thought of implicit promotion but realized it doesn't matter in this specific case. Or the programmer did not think of implicit promotion and just got the code right by luck. Whereas best practice myNum = (uint16_t)~myNum; means that the programmer did definitely consider it and in addition this will silence signed to unsigned implicit conversion warnings from the compiler/static analyzer.