49

在 C 中,当左侧操作数为负值时,按位左移操作调用未定义行为。

来自 ISO C99 (6.5.7/4) 的相关引用

E1 << E2 的结果是 E1 左移 E2 位位置;空出的位用零填充。如果 E1 具有无符号类型,则结果的值为 E1 × 2 E2,以比结果类型中可表示的最大值多一个模数减少。如果 E1 有带符号类型和非负值,并且 E1 × 2 E2在结果类型中是可表示的,那么这就是结果值;否则,行为未定义

但是在 C++ 中,行为是明确定义的。

ISO C++-03 (5.8/2)

E1 << E2 的值是 E1(解释为位模式)左移 E2 位位置;空出的位用零填充。如果 E1 具有无符号类型,则结果的值是 E1 乘以 2 的 E2 次幂,如果 E1 具有无符号长类型,则以模 ULONG_MAX+1 减少,否则为 UINT_MAX+1。[注意:常量 ULONG_MAX 和 UINT_MAX 在标题中定义)。]

这意味着

int a = -1, b=2, c;
c= a << b ;

在 C 中调用未定义的行为,但该行为在 C++ 中定义良好。

是什么迫使 ISO C++ 委员会认为这种行为与 C 中的行为相反?

另一方面,implementation defined当左操作数为负时,行为是按位右移操作,对吗?

我的问题是为什么左移操作会调用 C 中的未定义行为,为什么右移运算符只调用实现定义的行为?

PS:请不要给出“这是未定义的行为,因为标准是这样说的”这样的答案。:P

4

8 回答 8

39

您复制的段落是关于无符号类型的。该行为在 C++中未定义。来自上一个 C++0x 草案:

E1 << E2 的值是 E1 左移 E2 位位置;空出的位用零填充。如果 E1 具有无符号类型,则结果的值为 E1 × 2 E2,比结果类型中可表示的最大值多模一减少。否则,如果 E1 具有带符号类型和非负值,并且 E1×2 E2在结果类型中是可表示的,那么这就是结果值;否则,行为是 undefined

编辑:看看 C++98 论文。它根本没有提到签名类型。所以它仍然是未定义的行为。

右移负数是实现定义的,对。为什么?在我看来:实现定义很容易,因为左边的问题没有截断。当您向左移动时,您不仅必须说出从右侧移动的内容,而且还必须说出其余位发生的情况,例如使用二进制补码表示,这是另一回事。

于 2010-09-24T07:46:46.893 回答
20

In C bitwise left shift operation invokes Undefined Behaviour when the left side operand has negative value. [...] But in C++ the behaviour is well defined. [...] why [...]

The easy answer is: Becuase the standards say so.

A longer answer is: It has probably something to do with the fact that C and C++ both allow other representations for negative numbers besides 2's complement. Giving fewer guarantees on what's going to happen makes it possible to use the languages on other hardware including obscure and/or old machines.

For some reason, the C++ standardization committee felt like adding a little guarantee about how the bit representation changes. But since negative numbers still may be represented via 1's complement or sign+magnitude the resulting value possibilities still vary.

Assuming 16 bit ints, we'll have

 -1 = 1111111111111111  // 2's complement
 -1 = 1111111111111110  // 1's complement
 -1 = 1000000000000001  // sign+magnitude

Shifted to the left by 3, we'll get

 -8 = 1111111111111000  // 2's complement
-15 = 1111111111110000  // 1's complement
  8 = 0000000000001000  // sign+magnitude

What forced the ISO C++ committee to consider that behaviour well defined as opposed to the behaviour in C?

I guess they made this guarantee so that you can use << appropriately when you know what you're doing (ie when you're sure your machine uses 2's complement).

On the other hand the behaviour is implementation defined for bitwise right shift operation when the left operand is negative, right?

I'd have to check the standard. But you may be right. A right shift without sign extension on a 2's complement machine isn't particularly useful. So, the current state is definitely better than requiring vacated bits to be zero-filled because it leaves room for machines that do a sign extensions -- even though it is not guaranteed.

于 2010-09-24T18:02:26.850 回答
7

要回答标题中所述的真正问题:对于有符号类型的任何操作,如果数学运算的结果不适合目标类型(不足或溢出),则此行为未定义。有符号整数类型就是这样设计的。

对于左移操作,如果值为正或 0,则将运算符定义为乘以 2 的幂是有意义的,所以一切正常,除非结果溢出,这不足为奇。

如果该值为负数,您可以对乘以 2 的幂有相同的解释,但如果您只考虑位移,这可能会令人惊讶。显然,标准委员会想要避免这种模棱两可的情况。

我的结论:

  • 如果您想进行真正的位模式操作,请使用无符号类型
  • 如果您想将一个值(有符号或无符号)乘以 2 的幂,请执行此操作,例如

    我 * (1u << k)

无论如何,您的编译器都会将其转换为体面的汇编程序。

于 2010-09-24T08:18:20.047 回答
3

许多此类事情是在普通 CPU 可以在单个指令中实际支持的内容与足以期望编译器编写者保证即使需要额外指令的有用之间取得平衡。通常,使用位移运算符的程序员希望它们映射到具有此类指令的 CPU 上的单个指令,这就是为什么存在未定义或实现行为的原因,其中 CPU 对“边缘”条件进行了各种处理,而不是强制执行行为并进行操作出乎意料的慢。请记住,即使对于更简单的用例,也可能会制作额外的前/后或处理说明。

于 2010-09-24T07:35:40.120 回答
1

我的问题是为什么左移操作会调用 C 中的未定义行为,为什么右移运算符只调用实现定义的行为?

LLVM 的人们推测,由于指令在各种平台上的实现方式,移位运算符受到限制。从每个 C 程序员应该知道的关于未定义行为 #1/3

...我的猜测是,这是因为各种 CPU 上的底层移位操作对此做了不同的事情:例如,X86 将 32 位移位量截断为 5 位(因此 32 位移位与移位相同) 0 位),但 PowerPC 将 32 位移位截断为 6 位(因此位移 32 产生零)。由于这些硬件差异,C 完全未定义行为...

Nate 讨论的是关于移位大于寄存器大小的数量。但这是我发现的最接近权威的解释转移限制的方法。

认为第二个原因是 2 的恭维机器上的潜在符号变化。但我从来没有在任何地方读过它(@sellibitze 没有冒犯(我碰巧同意他的观点))。

于 2014-04-05T17:03:50.817 回答
0

C++03 中的行为与 C++11 和 C99 中的行为相同,您只需要超越左移规则即可。

该标准的第 5p5 节说:

如果在计算表达式期间,结果未在数学上定义或不在其类型的可表示值范围内,则行为未定义

在 C99 和 C++11 中被特别称为未定义行为的左移表达式与计算结果超出可表示值范围的相同。

事实上,关于使用模运算的无符号类型的语句专门用于避免生成可表示范围之外的值,这将自动成为未定义的行为。

于 2014-07-19T20:06:33.510 回答
0

在 C89 中,左移负值的行为在二进制补码平台上明确定义,该平台不使用有符号和无符号整数类型的填充位。有符号和无符号类型的共同值位必须位于相同的位置,而有符号类型的符号位唯一可以放在的位置与无符号类型的上位值位相同,这反过来又必须在其他一切的左边。

C89 强制行为对于没有填充的二进制补码平台是有用且明智的,至少在将它们视为乘法不会导致溢出的情况下。该行为在其他平台上或在寻求可靠地捕获有符号整数溢出的实现上可能不是最佳的。C99 的作者可能希望在 C89 强制行为不太理想的情况下允许实现灵活性,但基本原理中没有任何内容表明质量实现不应该继续以旧方式运行的意图没有令人信服的理由不这样做。

不幸的是,尽管 C99 从未有任何不使用补码数学的实现,但 C11 的作者拒绝定义常见情况(非溢出)行为。IIRC,声称这样做会阻碍“优化”。当左侧操作数为负数时,让左移运算符调用未定义行为允许编译器假设仅当左侧操作数为非负数时才能进行移位。

我怀疑这种优化真正有用的频率,但这种有用性的稀有性实际上有利于使行为未定义。如果二进制补码实现不会以普通方式表现的唯一情况是优化实际上有用的情况,并且如果实际上不存在这种情况,那么无论是否有授权,实现都会以普通方式表现,并且没有需要强制行为。

于 2015-04-17T23:43:26.603 回答
-2

移位的结果取决于数字表示。仅当数字表示为二进制补码时,移位的行为类似于乘法。但问题并不只存在于负数。考虑一个以超过 8 表示的 4 位有符号数(也称为偏移二进制)。数字 1 表示为 1+8 或 1001 如果我们将其作为位左移,我们得到 0010,它是 -6 的表示。类似地,-1 表示为 -1+8 0111,左移后变为 1110,表示 +6。逐位行为是明确定义的,但数值行为高度依赖于表示系统。

于 2016-03-29T21:09:28.800 回答