Until C++20, the representation of signed integers is implementation-defined. However, std::intX_t
are guaranteed to have 2s'-complement representation even before C++20:
int8_t
, int16_t
, int32_t
, int64_t
- signed integer type with width of exactly 8, 16, 32 and 64 bits respectively with no padding bits and using 2's complement for negative values (provided only if the implementation directly supports the type)
When you write
std::int32_t iA = -1;
std::uint32_t uA = *(std::uint32_t*)&iA;
you get the value with all bits set. The standard says that accessing std::int32_t
through a pointer of type std::uint32_t*
is permitted if "type is similar to ... a type that is the signed or unsigned type corresponding to the dynamic type of the object". Thus, strictly speaking, we have to ensure that std::uint32_t
is indeed an unsigned type corresponding to std::int32_t
before dereferencing the pointer:
static_assert(std::is_same_v<std::make_unsigned_t<std::int32_t>, std::uint32_t>);
When you write
std::int32_t iB = -1;
std::uint32_t uB = (std::uint32_t)iB;
you rely on the conversion into the unsigned type that is well-defined and is guaranteed to produce the same value.
As for the assembly, both casts are no-ops:
std::uint32_t foo() {
std::int32_t iA = -1;
static_assert(std::is_same_v<std::make_unsigned_t<std::int32_t>, std::uint32_t>);
return *(std::uint32_t*)&iA;
}
std::uint32_t bar() {
std::int32_t iB = -1;
return (std::uint32_t)iB;
}
result in:
foo():
mov eax, -1
ret
bar():
mov eax, -1
ret