关于左移,有符号整数现在的行为是否有所不同?

does signed integers now behave differently, with regards to left shift?

在 c++20 中,有符号整数现在被定义为使用二进制补码,
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0907r3.html

这是一个可喜的变化,但是其中一个要点引起了我的注意:

Change Left-shift on signed integer types produces the same results as left-shift on the corresponding unsigned integer type.

这似乎是一个奇怪的变化。这不会移走符号位吗?

The C++17 wording 有符号左移 (E1 << E2) 是:

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; otherwise, the behavior is undefined.

请注意,它说的是 "the corresponding unsigned type" 中的可表示性。因此,如果您有一个值为 0x7FFFFFFF 的 32 位有符号整数,并且将其左移 1,则产生的移位可以用 32 位 unsigned 整数 (0xFFFFFFFE) 表示.但是随后这个无符号值被转换为结果类型。并且转换一个无符号整数,其值对于相应的有符号类型来说太大是实现定义的。

总的来说,在 C++17 中,左移到符号位可能会通过实现定义的行为发生,即使这样也只有在您不移动到超出无符号结果类型的大小的情况下。超过它显然是 UB。

The C++20 wording,对于有符号和无符号整数,是:

The value of E1 << E2 is the unique value congruent to E1×2E2 modulo 2N, where N is the width of the type of the result.

整数同余模数基本上就是截掉模数以外的位。整数的"width"是explicitly defined as:

The range of representable values for a signed integer type is −2N−1 to 2N−1−1 (inclusive), where N is called the width of the type.

这意味着对于一个 32 位有符号整数,宽度为 31。因此移位结果的模数为 31 位,这会切断符号位,明确防止移入它。

所以在C++20中,我们有更严格的保证;实现可以 never 对符号位进行带符号的左移。这与 C++17 的不同之处仅在于实现 variance/UB 已明确定义为不会发生。

所以左移在 C++17 中没有被定义为移入符号位,而在 C++20 中被定义为不这样做。

这句话的确切含义可能是指负数左移现在有效,无论您进行多少移位,移位总是明确定义的,signed/unsigned 的措辞换档总体上是一样的。

是的,C++20 改变了左移有符号整数的行为。

使用 C++17,将正符号整数左移到符号位会调用实现定义的行为。1示例:

int i = INT_MAX;
int j = i << 1;    // implementation defined behavior with std < C++20

C++20 将此更改为 defined 行为,因为它要求 two's complement 表示有符号整数。2,3

使用 C++17,移动负符号整数会调用 undefined 行为。1 示例:

int i = -1;
int j = i << 1;    // undefined behavior with std < C++20

在 C++20 中,这也发生了变化,现在这个操作也调用了 defined 行为。3

This seem like a strange change. Will this not shift away the sign bit?

是的,带符号的左移移走了符号位。示例:

int i = 1 << (sizeof(int)*8-1);    // C++20: defined behavior, set most significant bit
int j = i << 1;                    // C++20: defined behavior, set to 0 

将某些行为指定为未定义或实现定义行为的主要原因是允许在不同硬件上高效实现。

如今,由于所有 CPU 都实现了 two's complement,因此 C++ 标准强制要求它是很自然的。而且,如果您强制执行二进制补码,那么您将上述操作定义为唯一的结果,因为这也是左移在所有二进制补码指令集体系结构 (ISA) 中的行为方式。

IOW,让它实现已定义和未定义不会给你带来任何好处。

或者,如果您喜欢以前的未定义行为,您为什么会关心它是否已更改为已定义行为?您仍然可以像以前一样避免此操作。您不必更改代码。


1

The value of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1 × 2**E2, reduced modulo one more than the maximum value representable in the result type. Otherwise, if E1 has a signed type and non-negative value, and E1 × 2**E2 is representable in the corresponding unsigned type of the result type, then that value, converted to the result type, is the resulting value; otherwise, the behavior is undefined.

C++17 final working draft,第 8.8 节移位运算符 [expr.shift],第 2 段,第 132 页 - 强调我的)

2

[..] For each value x of a signed integer type, the value of the corresponding unsigned integer type congruent to x modulo 2 N has the same value of corresponding bits in its value representation. 41) This is also known as two’s complement representation. [..]

(C++20 latest working draft,第 6.8.1 节基本类型 [basic.fundamental],第 3 段,第 66 页)

3

The value of E1 << E2 is the unique value congruent to E1 × 2**E2 modulo 2**N, where N is the width of the type of the result. [Note: E1 is left-shifted E2 bit positions; vacated bits are zero-filled. — end note]

(C++20 latest working draft,第 7.6.7 节移位运算符 [expr.shift],第 2 段,第 129 页,link 我的)