有符号和无符号操作数进行位运算 '&' 的区别

31

我遇到了一个有趣的情况,根据正确的操作数类型,我得到了不同的结果,但我真的无法理解其中的原因。

以下是最小化的代码:

#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;
}

我使用 Linux 64 位系统中的g++(gcc版本为4.5.2)编译了以下代码:g++ -std=c++0x -Wall example.cpp -o example

输出结果如下:

ffffffff81230000

81230000

我无法理解第一种情况输出结果的原因。

为什么在某些时候,任何时间计算结果都会升级为 带符号64位 值 (int64_t) 导致符号扩展?

如果16位值首先左移16位然后被提升为64位值,则我会接受两种情况下都返回 '0' 的结果。如果编译器首先将check 提升为 uint64_t 然后执行其他操作,则我也能接受第二个输出结果。

但是,为什么 & 使用 0xFFFF (int32_t) vs. 0xFFFFU (uint32_t) 会导致这两个不同的输出结果呢?


使用两个Windows编译器无法重现,第一个掩码使用0xFFFFll(确保64位)。 - Cheers and hth. - Alf
1
@Cheersandhth.-Alf:这是可以预料的。如果从最大类型开始,较小的类型会被提升为较大的类型,避免了这种提升。 - MSalters
@Cheersandhth.-Alf:我刚在x86上的Visual Studio 2015中重现了这个问题。check & 0xFFFF返回0x00008123(check & 0xFFFF) << 16在立即窗口中返回0x81230000,而(uint64_t)((check & 0xFFFF) << 16)则返回0xffffffff81230000 - vgru
1
@AlexLop:小心使用术语。实际上,“signed”是一种类型名称,它是“signed int”的简写,也就是“int”。这也是整数常量(如“0xFFFF”)的默认类型。 - MSalters
@MSalters 你是对的... 我应该写成“有符号类型”。 - Alex Lop.
显示剩余4条评论
7个回答

23

这确实是一个有趣的边界情况。 因为你在架构使用32位的int时,你使用uint16_t作为无符号类型。

这里是来自 C++14 草案n4296 的 第5条表达式 的摘录(强调是我的):

10 许多需要算术或枚举类型的操作数的二元运算会引起转换... 这个模式被称为标准算术转换,其定义如下:
...
(10.5.3) — 否则,如果具有无符号整数类型的操作数的等级大于或等于另一个操作数的类型的等级,带有有符号整数类型的操作数应转换为具有无符号整数类型的操作数的类型。
(10.5.4) — 否则,如果带有有符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则带有无符号整数类型的操作数应转换为具有有符号整数类型的操作数的类型。

你处于第10.5.4种情况中:

  • uint16_t只有16位,而int有32位
  • int可以表示uint16_t的所有值

所以,操作数uint16_t check = 0x8123U被转换为有符号的0x8123,按位与的结果仍然是0x8123

但是移位(按位移动,因此在表示级别上发生)导致结果成为中间无符号的0x81230000,将其转换为 int 会得到一个负值(从技术上讲,这种转换是实现定义的,但这种转换是常见用法)

5.8位移运算符[expr.shift]
...
否则,如果E1具有带符号类型和非负值,并且E1×2E2在结果类型的相应无符号类型中是可表示的,则该值转换为结果类型后即为结果值; ...

并且

4.7整数转换[conv.integral]
...
3 如果目标类型为有符号类型,则如果它可以用目标类型表示,则该值不变;否则,该值为实现定义

(注意,这在C++11中是未定义行为...)

因此,你将有符号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×2E2在结果类型中是可表示的,则这将是结果的值; 否则,行为未定义


1
请注意,示例代码是以-std=c++0x编译的,即C++11草案,而不是C++14。 - MSalters
有趣。在C11中,它与C++11一样未定义。这是另一个例子,两种语言现在的差异。 - 2501

10
首先要意识到的是,对于内置类型,如a&b这样的二元运算符仅在两侧具有相同类型时才起作用。(对于用户定义的类型和重载,任何情况都可以)。这可能通过隐式转换实现。
现在,在您的情况下,肯定存在这样的转换,因为根本不存在一个小于int的二元运算符&。两个操作数都被转换为至少int大小,但确切的类型是什么?
恰好,在您的GCC上,int确实是32位的。这很重要,因为它意味着所有uint16_t的值都可以表示为一个int。没有溢出。
因此,check & 0xFFFF是一个简单的情况。右侧已经是一个int,左侧升级为int,因此结果是int(0x8123)。这完全没问题。
现在,下一个操作是0x8123 << 16。请记住,在您的系统上,int为32位,并且INT_MAX0x7FFF'FFFF。如果没有溢出,0x8123 << 16将是0x81230000,但显然比INT_MAX大,因此实际上发生了溢出。

C++11中的有符号整数溢出是未定义行为。任何结果都是正确的,包括purple或根本没有输出。至少你得到了一个数值,但是众所周知,GCC会直接消除必然导致溢出的代码路径。

[编辑]较新版本的GCC支持C++14,其中此特定形式的溢出已成为实现定义 - 请参见Serge的答案。


2
我的理解是标准中没有未定义的行为,只有一个实现相关的情况(请参见我的答案)。因此,purple不应该是一个可接受的值;-) - Serge Ballesta

10
让我们看一下...
uint64_t new_check = (check & 0xFFFF) << 16;

在这里,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;
}

输出

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

1
不,有符号结果的最高位不是1。 - Cheers and hth. - Alf
抱歉。对于32位有符号结果,MSB确实为1。即当int为32位时。 - Cheers and hth. - Alf
@Cheersandhth.-Alf ... 好的,但是 (uint64_t)0x80000000 == 0x0000000080000000ULL 不是吗? - Alex Lop.
@AlexLop:在32位的int中,0x80000000的类型是unsigned int。;-) - Cheers and hth. - Alf

2

0xFFFF 是一个有符号整数。所以在 & 操作之后,我们得到的是一个32位的有符号值:

#include <stdint.h>
#include <type_traits>

uint64_t foo(uint16_t a) {
  auto x = (a & 0xFFFF);
  static_assert(std::is_same<int32_t, decltype(x)>::value, "not an int32_t")
  static_assert(std::is_same<uint16_t, decltype(x)>::value, "not a uint16_t");
  return x;
}

http://ideone.com/tEQmbP

你原来的16位会被左移,生成32位值并设置最高位(0x80000000U),导致其成为负数。在64位转换过程中,符号扩展发生,填充高位字为1。


1
这是整数提升的结果。在执行 & 操作之前,如果操作数比一个 int(对于该架构而言)“小”,编译器将会将两个操作数都提升为 int,因为它们都适合于一个 signed int
这意味着第一个表达式等同于以下内容(在32位架构上):
// check is uint16_t, but it fits into int32_t.
// the constant is signed, so it's sign-extended into an int
((int32_t)check & (int32_t)0xFFFFFFFF)

另一个操作数将被提升为第二个操作数:

// check is uint16_t, but it fits into int32_t.
// the constant is unsigned, so the upper 16 bits are zero
((int32_t)check & (int32_t)0x0000FFFFU)

如果您将check显式转换为unsigned int,则两种情况的结果将相同(unsigned * signed将导致unsigned):
((uint32_t)check & 0xFFFF) << 16

将等于:

((uint32_t)check & 0xFFFFU) << 16

但是 uint16_t 是无符号的... unsigned X signed 仍应该得到无符号的结果... 对吧? - Alex Lop.
@AlexLop.:uint16_t也会被提升为signed int,因为它适合于signed int - vgru

1
你的平台使用32位的 int
你的代码与以下代码完全等价:
#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0x8123U;
    auto a1 = (check & 0xFFFF) << 16
    uint64_t new_check = a1;
    std::cout << std::hex << new_check << std::endl;

    auto a2 = (check & 0xFFFFU) << 16;
    new_check = a2;
    std::cout << std::hex << new_check << std::endl;
    return 0;
}

a1a2的类型是什么?

  • 对于a2,结果会提升为unsigned int
  • 更有趣的是,对于a1,结果会提升为int,然后在扩展为uint64_t时进行符号扩展。

这里有一个更短的演示,使用十进制表示,以便显示有符号和无符号类型之间的差异:

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0;
    std::cout << check
              << "  " << (int)(check + 0x80000000)
              << "  " << (uint64_t)(int)(check + 0x80000000) << std::endl;
    return 0;
}

在我的系统上(也是32位的int),我得到:
0  -2147483648  18446744071562067968

展示促销和符号扩展发生的位置。

如果你想表达“完全等同于”,那么也许 auto a1 = (check & 0x0000FFFF) 可能更加合适并且清晰明了。 - kfsone

0

& 操作有两个操作数。第一个是无符号短整型,它将经历通常的提升成为整型。第二个是常量,一个情况下是整型,另一个情况下是无符号整型。因此,& 的结果在一个情况下是整型,在另一个情况下是无符号整型。该值被左移,结果可能是带有符号位的整型,或者无符号整型。将负整型强制转换为 uint64_t 将得到一个较大的负整数。

当然,你应该始终遵循这个规则:如果你做了某事,而且你不理解结果,那就别做那件事!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接