有点'&' with signed vs unsigned操作数

时间:2016-08-03 07:25:06

标签: c++ c++11 bitwise-and type-promotion sign-extension

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

这是最小的代码:

#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 64bit上用g ++(gcc版本4.5.2)编译了这段代码: g ++ -std = c ++ 0x -Wall example.cpp -o example

输出结果为:

  

ffffffff81230000

     

81230000

在第一种情况下,我无法理解输出的原因。

为什么在某些时候将任何时态计算结果提升为有符号64位值(int64_t),从而导致符号扩展?

我会接受&#39; 0&#39;的结果。在这两种情况下,如果16位值在第一位左移16位,然后提升为64位值。如果编译器首先将check提升为uint64_t,然后执行其他操作,我也会接受第二个输出。

但是&如何使用0xFFFF(int32_t)与0xFFFFU(uint32_t)会产生这两种不同的输出?

7 个答案:

答案 0 :(得分:21)

这确实是一个有趣的角落案例。它只出现在这里,因为当架构使用32位uint16_t

时,您使用ìnt作为无符号类型

以下是来自草案n4296 for C ++ 14的第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给出负值(从技术上讲,它是实现定义的,但这种转换是常见用法)< / p>

  

5.8移位运算符[expr.shift]
...
否则,如果E1具有有符号类型且非负值,则E1×2 E2 为< STRONG>表示的   在结果类型的相应无符号类型中,那么转换为结果类型的值是   结果价值; ......

  

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 可表示   在结果类型中,那就是结果值; 否则,行为未定义

答案 1 :(得分:10)

让我们来看看

uint64_t new_check = (check & 0xFFFF) << 16;

此处,0xFFFF是有符号常量,因此(check & 0xFFFF)通过整数提升规则为我们提供有符号整数。

在你的情况下,使用32位int类型,左移后的此整数的MSbit为1,因此64位无符号的扩展将执行符号扩展,将位填充到离开了1&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

答案 2 :(得分: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大,所以实际上有溢出。

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

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

答案 3 :(得分:2)

0xFFFF是一个签名的int。因此,在&操作之后,我们有一个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位转换符号扩展期间,用1s填充上面的单词。

答案 4 :(得分:1)

这是整数提升的结果。在&操作发生之前,如果操作数是&#34;更小&#34;对于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

答案 5 :(得分: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

显示促销和签名延期的位置。

答案 6 :(得分:0)

&amp;操作有两个操作数。第一个是未签约的短片,它将通过促销成为一个int。第二个是常量,在一个类型为int的情况下,在另一个类型为unsigned int的情况下。 &amp;结果因此在一种情况下是int,在另一种情况下是unsigned int。该值向左移动,导致符号位置位的int或unsigned int。将负int转换为uint64_t将给出一个较大的负整数。

当然,你应该始终遵循这样的规则:如果你做了什么,而你不了解结果,那么就不要这样做!