带符号与无符号操作数的按位“与”

Bit wise '&' with signed vs unsigned operand

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

这是最小代码:

#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) 与 0xFFFFU (uint32_t) 为什么会导致这两个不同的输出?

这是整数提升的结果。在 & 操作发生之前,如果操作数是 "smaller" 而不是 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

一起来看看

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

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 位左移,得到高位设置 (0x80000000U) 的 32 位值,因此它具有负值。在 64 位转换期间发生符号扩展,用 1 填充高位字。

& 操作有两个操作数。第一个是未签名的 short,它将经过通常的提升成为一个 int。第二个是常量,在一种情况下是 int 类型,在另一种情况下是 unsigned int 类型。因此 & 的结果在一种情况下是 int,在另一种情况下是 unsigned int。该值向左移动,结果要么是设置了符号位的 int,要么是 unsigned int。将负整数转换为 uint64_t 将给出一个大的负整数。

当然你应该始终遵循规则:如果你做了某事,但你不了解结果,那就不要做!

首先要意识到的是,像 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,因此实际上存在溢出。

Signed integer overflow in C++11 is Undefined Behavior。从字面上看,任何结果都是正确的,包括 purple 或根本没有输出。至少你得到了一个数值,但众所周知,GCC 会彻底消除不可避免地导致溢出的代码路径。

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

您的平台有 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

显示促销和标志扩展发生的位置。

这确实是一个有趣的极端案例。它只出现在这里,因为当您的体系结构将 32 位用于 ìnt

时,您将 uint16_t 用于无符号类型

这是 C++14 草案 n4296 中 条款 5 表达式 的摘录(强调我的):

10 Many binary operators that expect operands of arithmetic or enumeration type cause conversions ... This pattern is called the usual arithmetic conversions, which are defined as follows:
...
(10.5.3) — Otherwise, if the operand that has unsigned integer type has rank greater than or equal to the rank of the type of the other operand, the operand with signed integer type shall be converted to the type of the operand with unsigned integer type.
(10.5.4) — Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, the operand with unsigned integer type shall be converted to the type of the operand with signed integer type.

您属于10.5.4案例:

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

所以 uint16_t check = 0x8123U 操作数被转换为带符号的 0x8123 并且按位 & 的结果仍然是 0x8123.

但是移位(按位发生在表示级别)导致结果成为中间无符号 0x81230000,它转换为 int 给出负值(技术上它是实现定义的,但这种转换是常见用法)

5.8 Shift operators [expr.shift]
...
Otherwise, if E1 has a signed type and non-negative value, and E1×2E2 is representable in the corresponding unsigned type of the result type, then that value, converted to the result type, is the resulting value;...

4.7 Integral conversions [conv.integral]
...
3 If the destination type is signed, the value is unchanged if it can be represented in the destination type; otherwise, the value is implementation-defined.

(注意这是 C++11 中真正的未定义行为...)

所以你以将带符号的 int 0x81230000 转换为 uint64_t 结束,这正如预期的那样给出了 0xFFFFFFFF81230000,因为

4.7 Integral conversions [conv.integral]
...
2 If the destination type is unsigned, the resulting value is the least unsigned integer congruent to the source integer (modulo 2n where n is the number of bits used to represent the unsigned type).

TL/DR:这里没有未定义的行为,导致结果的是有符号32位int到无符号64位int的转换。 未定义行为的唯一部分是一个会导致符号溢出的转变,但所有常见的实现都共享这个,它是实现定义 C++14 标准。

当然,如果您强制第二个操作数是无符号的,则所有内容都是无符号的,您显然会得到正确的 0x81230000 结果。

[编辑] 正如 MSalters 所解释的那样,自 C++14 以来,转变的结果只是 实现定义,但确实是 未定义的行为 在 C++11 中。移位运算符段落说:

...
Otherwise, if E1 has a signed type and non-negative value, and E1×2E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.