为什么在C++14中1 << 31被改为实现定义?

34
在2014年之前的所有C和C++版本中,写下以下代码:
1 << (CHAR_BIT * sizeof(int) - 1)

由于左移被定义为相当于连续乘以2,因此这种移位导致了未定义的行为,因为它导致了有符号整数溢出:

E1 << E2 的结果是 E1 左移 E2 位; 空出的位用零填充。[...] 如果 E1 具有有符号类型和非负值,并且 E1 × 2E2 可以表示为结果类型中的值,则该值就是结果;否则,行为是未定义的

然而,在 C++14 中,左移的文本已更改,但乘法未更改:

E1 << E2 的值是 E1 左移 E2 位; 空出的位填充为零。[...] 否则,如果 E1 具有有符号类型和非负值,并且 E1 × 2E2 可以表示为结果类型的对应无符号类型中的值,则将其转换为结果类型后得到的值是结果;否则,行为是未定义的。

现在的行为与对有符号类型的超出范围赋值相同,即由 [conv.integral]/3 覆盖:

如果目标类型是有符号的,则当它可以在目标类型(和位域宽度)中表示时,该值不变;否则,该值是实现定义的

这意味着在写 1 << 31(在具有32位 int 的系统上)时仍然是不可移植的。那么为什么 C++14 进行了这种更改?


2
+1 Howard Hinnant。在此主题上的评论,我记得那个评论的唯一原因是那个评论是我这个问题的灵感来源之一。 - Shafik Yaghmour
想象一下,如果他们只是简单地陈述了像这样的内容:“限制条件:在升级后,左操作数 E1 必须是无符号整数类型”,那么就可以避免大量微妙的移位相关错误,从而拯救人类。 - Lundin
1个回答

20
相关问题是CWG 1457,其正当理由是更改允许在常量表达式中使用1 << 31

5.8 [expr.shift]第2段的当前措辞使得通过将(有符号的)1左移进符号位来创建给定类型的最小负整数成为未定义行为,即使这种做法并不罕见,并且在大多数(二进制补码)架构上可以正确地工作:

如果E1具有带符号类型和非负值,并且E1 * 2 E2 在结果类型中可表示,则该值为结果;否则,行为未定义。

因此,这种技术无法用于常量表达式,这将破坏大量代码。

常量表达式不能包含未定义行为,这意味着在需要常量表达式的上下文中使用包含UB的表达式会使程序不合法。例如,libstdc++的numeric_limits::min 曾因此无法在clang中编译通过


那段文字的作者似乎很困惑...新规则也不允许通过将(有符号的)1左移至符号位来创建给定类型的最负整数。结果是实现定义的,这与保证给出最负值相去甚远。 - Ben Voigt
3
@Loopunroller:问题中最后一个标准引用的措辞是为了避免要求将无符号值转换为有符号值时遵循二进制补码规则。此外,对于二进制补码而言,并不是只有一种合理的结果。饱和算术也是一个有用且合理的选择。甚至可以说,这比将正值加倍导致负值更加合理。 - Ben Voigt
1
他们真的觉得写“- max() - 1”很痛苦,所以决定改变标准吗? - M.M
13
+1 Fwiw,libc ++是通过使用min()计算max()的,因此使用max()来计算min()会过于有趣。 :-) 总之,就我而言:我已经厌倦了为不受任何现代C++编译器支持的架构提供支持。二进制补码有符号整数是我们现在和未来的现实。 - Howard Hinnant
@Columbo:某件事情可能只有一种合理的行为方式,但这并不能阻止编译器实现疯狂的行为。 - supercat
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接