C++ Not-Zero Equality Trap
What would you expect this C++ code to do?
uint8_t val = ~0;
If (val == ~0) {
printf("YES!");
} else {
printf("NO!");
}
The result: NO!
How about this?
#define DO_TEST( TEST_TYPE ) \
{ \
TEST_TYPE val = ~0; \
if (val == ~0 ) { \
printf(#TEST_TYPE ": equal to ~0\n"); \
} else { \
printf(#TEST_TYPE ": NOT equal to ~0\n"); \
} \
} \
DO_TEST(uint8_t);
DO_TEST(uint16_t);
DO_TEST(uint32_t);
DO_TEST(uint64_t);
DO_TEST(int8_t);
DO_TEST(int16_t);
DO_TEST(int32_t);
DO_TEST(int64_t);
The result:
uint8_t: NOT equal to ~0
uint16_t: NOT equal to ~0
uint32_t: equal to ~0
uint64_t: equal to ~0
int8_t: equal to ~0
int16_t: equal to ~0
int32_t: equal to ~0
int64_t: equal to ~0
What is so different about uint8_t
and uint16_t
?
… And why do all the signed types seem unaffected?
Let’s update the test code to use a literal size-suffix:
if (val == ~0ll ) {
This will ensure that the literal is a 64-bit value. The new result:
uint8_t: NOT equal to ~0
uint16_t: NOT equal to ~0
uint32_t: NOT equal to ~0
uint64_t: equal to ~0
int8_t: equal to ~0
int16_t: equal to ~0
int32_t: equal to ~0
int64_t: equal to ~0
The uint32_t
test now fails also.
What is going on here?
The bitwise negation operator, ~
, applied to 0
results in a number with all bits set, regardless of the signed-ness.
Let’s write the uint8_t
expression another way by making Val
a literal as well:
0xff == ~0
This evaluates to false
also.
Let’s also expand out the ~0
to the 32bit default int size.
0xff == 0xffffffff
We can see more clearly now that the values are indeed not equal.
But why does it succeed when val
has more bits than the 32 of default int?
… Automatic Integral Promotion!
Integral promotion is effective in ensuring that intermediate calculations involving smaller types do not overflow. In the case of the equality operator, both operands are expanded to the size of the larger of the two.
Let’s expand the expression for the uint_64
case:
0xffffffffffffffff == ~0x0000000000000000
After applying the binary negation, it is clearly true
:
0xffffffffffffffff == 0xffffffffffffffff
But what about the signed cases, which always work regardless of integral promotion?
Let’s look at the binary representation of -1
with varying type sizes:
8-bits: 11111111
16-bits: 1111111111111111
32-bits: 11111111111111111111111111111111
The first bit indicates that this is a negative number. These negative numbers simply are interpretations of unsigned numbers that wrap around when counting below zero. This representation is known as Two’s Complement.
When integral promotion is applied, the numerical value of the negative number is maintained. On a binary level, this is effectively done by repeating the first, “sign bit”, to fill in the newly added bytes.
Although ~0
stored in a uint8_t
and a int8_t
both have the same binary representation, the method of integral promotion applied by the compiler differs.
Conclusion
Integral promotion in the expression comparing the two values occurs before the ~
operator. Both the ~0
and val
are promoted to the largest type between the two. If val
is the larger type, the condition can succeed; otherwise, it will always fail.
In the case of negative numbers, integral promotion extends the sign bit to maintain the same numerical value in two’s complement encoding. This results in the values continuing to be equal after promotion.