Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compiler changes the type variable type from uin16_t to int when it's marked as constexpr

Tags:

c++

constexpr

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?

like image 563
bielu000 Avatar asked Oct 06 '21 11:10

bielu000


2 Answers

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:

  • expr.conv.prom-7.3.7 - The first few paragraphs.
  • expr.unary.op-7.6.2.2.10 - The last paragraph, states that "Integer promotion" is applied.
  • expr.arith.conv-7.4 - Only applies for binary operators, still contains similar rules.
like image 56
Quimby Avatar answered Oct 26 '22 23:10

Quimby


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.

like image 36
Andreas Wenzel Avatar answered Oct 27 '22 01:10

Andreas Wenzel