【发布时间】:2019-01-18 14:08:55
【问题描述】:
在 c++20 中,现在将有符号整数定义为使用二进制补码,
见http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0907r3.html
这是一个受欢迎的变化,但其中一个要点引起了我的注意:
更改有符号整数类型的左移会产生相同的结果 结果为对应的无符号整数类型的左移。
这似乎是一个奇怪的变化。这不会移开符号位吗?
【问题讨论】:
在 c++20 中,现在将有符号整数定义为使用二进制补码,
见http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0907r3.html
这是一个受欢迎的变化,但其中一个要点引起了我的注意:
更改有符号整数类型的左移会产生相同的结果 结果为对应的无符号整数类型的左移。
这似乎是一个奇怪的变化。这不会移开符号位吗?
【问题讨论】:
The C++17 wording 用于有符号左移 (E1 << E2) 是:
否则,如果 E1 具有带符号类型和非负值,并且 E1×2E2 可以在结果类型的相应无符号类型中表示,则将该值转换为结果类型, 是结果值;否则,行为未定义。
请注意,它说的是可表示为“相应的 unsigned 类型”。因此,如果您有一个值为 0x7FFFFFFF 的 32 位有符号整数,并将其左移 1,则结果移位可以用 32 位 无符号 整数 (0xFFFFFFFE) 表示。但随后这个无符号值被转换为结果类型。并且转换一个对于相应的有符号类型来说值太大的无符号整数是实现定义的。
总体而言,在 C++17 中,左移到符号位可以通过实现定义的行为发生,即使这样,也只有在不超出无符号结果类型的大小的情况下才会发生。越过这就是明确的 UB。
The C++20 wording,对于有符号和无符号整数,是:
E1的值E2模2N,其中N是结果类型的宽度。
整数同余模数基本上意味着切断超出模数的位。整数的“宽度”为explicitly defined as:
有符号整数类型的可表示值的范围是 -2N-1 到 2N-1-1(含),其中 N 称为宽度类型。
这意味着对于一个 32 位的有符号整数,宽度是 31。所以移位结果的模数是 31 位,它切断了符号位,明确地阻止了移位。
所以在 C++20 中,我们有一个更难的保证;实现可以从不对符号位进行有符号左移。这与 C++17 的不同之处仅在于实现方差/UB 已明确定义为不会发生。
所以左移在 C++17 中没有被定义为移入符号位,在 C++20 中被定义为不这样做。
这句话的确切含义可能是指负数上的左移现在有效,无论您进行多少移位,移位始终是明确定义的,并且有符号/无符号移位的措辞总体上是一样。
【讨论】:
INT_MAX 值的 int 时,你移入符号位,结果是 -2。在 C++20 之前的 AFAIUI 这是未定义的行为,现在不是。
signed的宽度为 32;指数中有一个-1。所以INT_MAX/2+1<<1 == INT_MIN,因为 -2*31 与 2^31 模 2^32 和INT_MIN<<2 == 0 一致。逻辑是位操作适用于作为位序列的整数,而不考虑符号位的特殊意义,或者操作只是在有符号和无符号整数之间的自然同构下进行。
N=32。另请参阅 C++17,第 6.8.1 节,第 1 段 - 您已经引用了。另请参阅下一段:“无符号整数类型与相应的有符号整数类型具有相同的宽度 N。”因此,您的其余答案不成立。
是的,左移有符号整数的行为随着 C++20 的变化而改变。
在 C++17 中,将正符号整数左移到符号位会调用实现定义行为。1示例:
int i = INT_MAX;
int j = i << 1; // implementation defined behavior with std < C++20
C++20 将此更改为 已定义 行为,因为它要求 two's complement 表示有符号整数。2,3
在 C++17 中,移动带负号的整数会调用 undefined 行为。1 示例:
int i = -1;
int j = i << 1; // undefined behavior with std < C++20
在 C++20 中,这也发生了变化,该操作现在也调用 已定义 行为。3
这似乎是一个奇怪的变化。这不会移开符号位吗?
是的,有符号左移会移开符号位。示例:
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
E1 << E2的值是E1左移E2位;空出的位用零填充。如果 E1 有一个无符号 类型,结果的值为E1 × 2**E2,比中可表示的最大值多模1 结果类型。否则,如果 E1 具有带符号类型和非负值,并且E1 × 2**E2是可表示的 在结果类型的相应无符号类型中,那么转换为结果类型的值就是 结果值; 否则,行为未定义。
(C++17 final working draft,第 8.8 节移位运算符 [expr.shift],第 2 段,第 132 页 - 强调我的)
2
[..] 对于有符号整数类型的每个值 x, 对应的无符号整数类型与 x 模 2 N 具有相同的对应位值 其价值表示。 41) 这也称为二进制补码表示。 [..]
(C++20 latest working draft,第 6.8.1 节基本类型 [basic.fundamental],第 3 段,第 66 页)
3
E1 << E2的值是 congruent 到E1 × 2**E2 modulo 2**N的唯一值,其中 N 是 结果的类型。 [注:E1为左移E2位;空出的位用零填充。 ——尾注]
(C++20 latest working draft,第 7.6.7 节移位运算符 [expr.shift],第 2 段,第 129 页,链接我的)
【讨论】: