Ale*_*op. 28 c++ bitwise-and c++11 sign-extension type-promotion
我遇到了一个有趣的场景,根据正确的操作数类型我得到了不同的结果,我无法理解它的原因.
这是最小的代码:
#include <iostream>
#include <cstdint>
int main()
{
uint16_t check = 0x8123U;
uint64_t new_check = (check & 0xFFFF) << 16;
std::cout << std::hex << new_check << std::endl;
new_check = (check & 0xFFFFU) << 16;
std::cout << std::hex << new_check << std::endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
我在Linux 64bit上使用g ++(gcc版本4.5.2)编译了这段代码:g ++ -std = c ++ 0x -Wall example.cpp -o example
输出是:
ffffffff81230000
81230000
我无法真正理解第一种情况下输出的原因.
为什么在某些时候将任何时间计算结果提升为带符号的64位值(int64_t),从而导致符号扩展?
如果16位值在第一位左移16位然后提升到64位值,我会在两种情况下都接受'0'的结果.如果编译器首先提升checkto uint64_t然后执行其他操作,我也接受第二个输出.
但是如何&使用0xFFFF(int32_t)和0xFFFFU(uint32_t)会导致这两个不同的输出?
Ser*_*sta 21
这确实是一个有趣的角落案例.它只出现在这里因为你uint16_t在架构使用32位时用于无符号类型ìnt
以下是来自C42 14草案n4296的第5章表达式的摘录(强调我的):
10许多期望算术或枚举类型操作数的二元运算符会导致转换......这种模式称为通常的算术转换,定义如下:
...
(10.5.3) - 否则,如果操作数具有无符号整数type的rank大于或等于另一个操作数的类型的rank,带有符号整数类型的operand应转换为带有无符号整数类型的operand类型.
(10.5.4) - 否则,如果带有符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数类型的所有值,则具有无符号整数类型的操作数应转换为操作数的类型带有符号整数类型.
您在10.5.4案例中:
uint16_t只有16位而int32 位int 可以代表所有的价值观 uint16_t因此uint16_t check = 0x8123U操作数转换为有符号0x8123,按位的结果&仍然是0x8123.
但是,移位(按位,因此它发生在表示级别)导致结果为中间无符号0x81230000,转换为int给出负值(从技术上讲,它是实现定义的,但此转换是常见用法)
5.8移位运算符[expr.shift]
...
否则,如果E1具有有符号类型和非负值,并且E1×2 E2可在结果类型的相应无符号类型中表示,则该值转换为结果type,是结果值; ...
和
4.7积分转换[conv.integral]
...
3如果目标类型已签名,则该值如果可以在目标类型中表示,则不会更改; 否则,该值是实现定义的.
(注意这是C++ 11中真正的未定义行为......)
所以你最后将signed int 0x81230000转换uint64_t为预期的0xFFFFFFFF81230000,因为
4.7积分转换[conv.integral]
...
2如果目标类型是无符号的,则结果值是与源整数一致的最小无符号整数(模2n,其中n是用于表示无符号类型的位数).
TL/DR:这里没有未定义的行为,导致结果的是将带符号的32位int转换为无符号的64位int.唯一的部分是未定义的行为是一个会导致符号溢出的转换,但是所有常见的实现都共享这个,它是在C++ 14标准中定义的实现.
当然,如果你强制第二个操作数是无符号的,那么一切都是无符号的,你明显得到了正确的0x81230000结果.
[编辑]正如MSalters所解释的那样,移位的结果只是自C++ 14以来定义的实现,但在C++ 11中确实是未定义的行为.班次操作员段落说:
...
否则,如果E1具有有符号类型和非负值,并且E1×2 E2 在结果类型中可表示,那么这就是结果值; 否则,行为未定义.
Ris*_*aje 10
我们来看看吧
uint64_t new_check = (check & 0xFFFF) << 16;
Run Code Online (Sandbox Code Playgroud)
这里0xFFFF是一个有符号常量,因此(check & 0xFFFF)通过整数提升规则给出一个有符号整数.
在你的情况下,对于32位int类型,左移后的这个整数的MSbit是1,因此64位无符号的扩展将执行符号扩展,用1填充左边的位.解释为二进制补码表示,给出相同的负值.
在第二种情况下,0xFFFFU是无符号的,因此我们得到无符号整数,左移位运算符按预期工作.
如果您的工具链支持__PRETTY_FUNCTION__最方便的功能,您可以快速确定编译器如何感知表达式类型:
#include <iostream>
#include <cstdint>
template<typename T>
void typecheck(T const& t)
{
std::cout << __PRETTY_FUNCTION__ << '\n';
std::cout << t << '\n';
}
int main()
{
uint16_t check = 0x8123U;
typecheck(0xFFFF);
typecheck(check & 0xFFFF);
typecheck((check & 0xFFFF) << 16);
typecheck(0xFFFFU);
typecheck(check & 0xFFFFU);
typecheck((check & 0xFFFFU) << 16);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
void typecheck(const T &) [T = int]
65535
void typecheck(const T &) [T = int]
33059
void typecheck(const T &) [T = int]
-2128412672
void typecheck(const T &) [T = unsigned int]
65535
void typecheck(const T &) [T = unsigned int]
33059
void typecheck(const T &) [T = unsigned int]
2166554624
Run Code Online (Sandbox Code Playgroud)
MSa*_*ers 10
首先要意识到的是,a&b内置类型的二元运算符只有在双方都具有相同类型时才有效.(使用用户定义的类型和重载,任何事情都会发生).这可以通过隐式转换来实现.
现在,在你的情况下,确实存在这样的转换,因为根本没有二进制运算符&采用小于的类型int.双方都被转换为至少int大小,但具体类型是什么?
碰巧,你的GCC int确实是32位.这很重要,因为它意味着所有的值uint16_t都可以表示为int.没有溢出.
因此,这check & 0xFFFF是一个简单的案例.右侧已经是int,左侧是促进int,所以结果是int(0x8123).这很好.
现在,下一步是0x8123 << 16.请记住,在您的系统上int是32位,并且INT_MAX是0x7FFF'FFFF.在没有溢出的情况下,0x8123 << 16会是0x81230000,但这显然比INT_MAX实际上溢出更大.
C++ 11中的带符号整数溢出是未定义行为.从字面上看,任何结果都是正确的,包括purple或根本没有输出.至少你得到了一个数值,但众所周知GCC会彻底消除不可避免地导致溢出的代码路径.
[编辑]较新的GCC版本支持C++ 14,其中这种特殊形式的溢出已经变为实现定义 - 请参阅Serge的回答.