I've encountered a weird problem trying to flip all bits of my number.
#include <cstdint>
constexpr uint16_t DefaultValueForPortStatus { 0xFFFF };
void f(uint16_t x)
{
}
int main()
{
f(~(DefaultValueForPortStatus));
}
When I'm compiling this program (GCC trunk) I'm getting an error:
warning: unsigned conversion from 'int' to 'uint16_t' {aka 'short unsigned int'} changes value from '-65536' to '0' [-Woverflow]
When I remove constexpr from type specifier, then no warning appears. Why is that? Why does the uint16_t constexpr variable is being changed by the compiler to int, whereas in the case of non-constexpr everything is fine?
This is caused by IMHO a quite unfortunate C++ rule about integer promotion. It basically states that all types smaller than int
are always promoted to int
if int
can represent all values of the original type. Only if not, unsigned int
is chosen. std::uint16_t
on standard 32/64-bit architectures falls into the first category.
int
is guaranteed to be at least 16-bit wide, if that would happen to be the case, unsigned int
would have been chosen, so the behaviour of the code is implementation-defined.
I do not know precisely why the compiler issues the warning only for constexpr
values, most likely because it could easily propagate that constant through the ~
. In other cases, someone might change DefaultValueForPortStatus
to some "safe" value that won't overflow when negated and converted from int
back to std::uint16_t
. But the issue is there regardless of constness, you can test it with this code:
#include <type_traits>
#include <cstdint>
constexpr uint16_t DefaultValueForPortStatus { 0xFFFF };
int main()
{
auto x = ~(DefaultValueForPortStatus);
static_assert(std::is_same_v<decltype(x), int>);
}
Relevant Standard sections:
Due to integer promotion, on platforms on which int
is 32 bits, the expression
~(DefaultValueForPortStatus)
evaluates to an int
with the value -65536
.
What exactly happens is the following:
Before the bitwise-NOT (~
) operation takes place, the operand DefaultValueForPortStatus
gets promoted to an int
, so that its memory representation will be equivalent to that of an unsigned int
with the following value:
0x0000FFFF
After applying the bitwise-NOT (~
) operator to it, its memory representation will be equivalent to that of an unsigned int
with the following value:
0xFFFF0000
However, the result has the data type int
, not unsigned int
. (I am only using the equivalent values of unsigned int
for illustrating the memory representation.) Therefore, the actual value of the result is -65536
(because C++ requires that two's complement memory representation is used for signed integers).
When converting this int
to uint16_t
to match the type of the function parameter, the 16 most significant bits are discarded and the 16 least significant bits are preserved. Therefore, the function argument will have the value 0
.
When compiling using default settings, gcc provides a warning of such truncations, if they occur in a constant expression. This warning can be disabled with the -Wno-overflow
command line option.
The reason why the compiler warns by default is probably because it assumes that such truncations are not intended to occur in constant expressions. It does not make this assumption with expressions that are based on non-const
variables.
The truncation will occur either way, whether the compiler issues a warning or not.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With