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如果编译器首先提升touint64_t然后执行其他操作,我也会接受第二个输出。

但是&0xFFFF ( int32_t) 与 0xFFFFU ( uint32_t) 怎么会导致这两个不同的输出呢?

4

7 回答 7

23

这确实是一个有趣的极端案例。它只发生在这里,因为uint16_t当您的体系结构使用 32 位时,您使用无符号类型ìnt

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

10 许多期望算术或枚举类型的操作数的二元运算符会导致转换...这种模式称为通常的算术转换,其定义如下:
...
(10.5.3) — 否则,如果操作数具有无符号整数type 的秩大于或等于另一个操作数的类型的秩,带符号整数类型的操作数应转换为无符号整数类型的操作数的类型。
(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可以在结果类型的相应无符号类型中表示,则将该值转换为结果类型,是结果值;...

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×2 E2在结果类型中是可表示的,那么这就是结果值;否则,行为是 undefined

于 2016-08-03T08:18:51.310 回答
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
于 2016-08-03T07:36:05.440 回答
10

首先要意识到的是,像a&b内置类型这样的二元运算符只有在双方具有相同类型时才有效。(使用用户定义的类型和重载,一切都会发生)。这可以通过隐式转换来实现。

现在,在您的情况下,肯定存在这样的转换,因为根本没有二元运算符&采用小于int. 双方都转换为至少int大小,但确切的类型是什么?

碰巧的是,在您的 GCCint上确实是 32 位。这很重要,因为这意味着 的所有值uint16_t都可以表示为int. 没有溢出。

因此,check & 0xFFFF是一个简单的案例。右边已经是一个int,左边提升到int,所以结果是int(0x8123)。这很好。

现在,下一个操作是0x8123 << 16。请记住,在您的系统int上是 32 位,并且INT_MAX0x7FFF'FFFF. 在没有溢出的情况下,0x8123 << 16会是0x81230000,但这显然比INT_MAX实际上溢出更大。

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

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

于 2016-08-03T07:55:35.477 回答
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 填充高位字。

于 2016-08-03T07:51:25.360 回答
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;
}

a1和的类型是a2什么?

  • 因为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-bit int)上,我得到

0  -2147483648  18446744071562067968

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

于 2016-08-03T08:16:38.570 回答
1

这是整数提升的结果。在&操作发生之前,如果操作数比int(对于该架构)“小”,编译器会将两个操作数提升为int,因为它们都适合 a 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
于 2016-08-03T07:29:10.770 回答
0

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

当然,您应该始终遵守规则:如果您做了某事,而您不了解结果,那就不要这样做!

于 2016-08-03T07:52:39.987 回答