在C++中,带符号整数溢出仍然是未定义行为吗?

98
我们知道,有符号整数溢出是未定义的行为。但在 C++11 的 cstdint 文档中有一些有趣的内容:

有符号整数类型,宽度分别为 8、16、32 和 64 位,没有填充位,并且对于负值使用2的补码(仅当实现直接支持该类型时提供)

查看链接

我的问题是:由于标准明确规定对于 int8_tint16_tint32_tint64_t,负数是采用2的补码,那么这些类型的溢出是否仍然是未定义的行为?

编辑 我查了一下 C++11 和 C11 标准,发现以下内容:

C++11,§18.4.1:

头文件定义了与 C 标准中的7.20相同的所有函数、类型和宏。

C11,§7.20.1.1:
typedef名称intN_t表示带有宽度N、没有填充位和二进制补码表示法的有符号整数类型。因此,int8_t表示具有确切8位宽度的这种有符号整数类型。

17
永远不要忘记,C++ 的唯一主要文档是标准。其他所有东西,即使像 CppReference 这样的维基百科,都是次要来源。这并不意味着它是错误的;只是不完全可靠。 - Nicol Bolas
我认为这将是未定义行为,因为在C语言中没有这些类型的例外情况,我不明白为什么C++会添加一个。 - Daniel Fischer
4
我有点困惑:在句子“分别具有8、16、32和64位宽度的带符号整数类型,没有填充位,并且对于负值使用二进制补码(仅在实现直接支持该类型时提供)”,这个句子中的动词在哪里?是否缺少一些内容?它是什么意思? - YSC
C++11基于C99,而不是C11。但无论如何这并不重要。 - L. F.
3个回答

94
这些类型的溢出仍然算是未定义行为吗?
是的。根据C++11标准第5/4段(关于任何表达式的一般性规定):
如果在计算表达式时,结果在数学上没有定义或者不在其类型可表示值范围内,那么行为是未定义的。[...]
使用二进制补码表示这些有符号类型并不意味着在计算这些类型的表达式时使用2^n取模算术。
另一方面,关于无符号算术,标准明确规定(第3.9.1/4段):
声明为unsigned的无符号整数应该遵守模2^n的算术律,其中n是该整数特定大小值表示中的位数。
这意味着无符号算术操作的结果始终"在数学上定义",并且结果始终在可表示范围内;因此,5/4不适用。注脚46解释了这一点:
这意味着,无符号算术不会溢出,因为不能用生成的无符号整数类型表示的结果会被减去一个数,这个数是大于生成的无符号整数类型可以表示的最大值的1。

1
这段文字也暗示了无符号溢出是未定义的,但实际上并非如此。 - Archie
9
不算完全准确,因为无符号数的定义是“模除”无符号范围。 - Lightness Races in Orbit
3
@Archie:我尝试过澄清,但基本上你已经从LightnessRacesinOrbit得到了答案。 - Andy Prowl
1
如果由于模运算而无法发生,则无符号溢出是否定义实际上并不重要... - Aconcagua
2
有一些无符号操作的结果并非“在数学上被定义”,特别是除以零,因此也许你在那个句子中的措辞并不完全是你想要表达的意思。我认为你的意思是当结果在数学上被定义时,它也在C++中被定义了。 - Toby Speight
1
相关的段落在c++20草案中是6.8.1.2和7.1.4。 - milahu

26

仅仅因为一个类型被定义为使用2s补码表示,并不意味着该类型中的算术溢出就变得有定义。

有符号算数溢出的未定义行为用于启用优化;例如,编译器可以假设如果 a > b,那么 a + 1 > b也成立。但在无符号算术中,由于 a + 1 可能会回绕到 0,所以需要进行第二个检查。此外,一些平台可以在算术溢出时生成陷阱信号(请参见例如 http://www.gnu.org/software/libc/manual/html_node/Program-Error-Signals.html);标准继续允许这种情况的发生。


6
值得注意的是,许多人更担心可能会出现的陷阱,但编译器假设实际上更加隐匿(这也是我希望在"实现定义"与"未定义行为"之间有一个类别的原因——与实现定义行为需要特定实现以一致的记录方式执行某些操作不同,我希望有一种"实现约束"行为,需要实现指定由某些操作造成的所有后果(规范可以明确包含未定义行为,但是...)。 - supercat
4
如果实际可行,鼓励编写更具体的实现(指不引起二进制溢出)。在硬件上,如果二进制补码数自然“环绕”,那么想要得到一个环绕整数结果的代码,没有理由执行许多指令来尝试在不发生整数溢出的情况下进行计算,因为硬件只需执行一两个指令即可完成。 - supercat
1
@supercat事实上,希望获得包装结果的代码(在二进制补码CPU上)可以将操作数强制转换为相应的无符号类型并执行操作(然后进行强制转换,获取实现定义值):这适用于加法,减法和乘法。唯一的问题是除法,模运算和诸如abs之类的函数。对于这些操作,当它工作时,它不需要比有符号算术更多的指令。 - Ruslan
@Ruslan:在代码需要精确包装结果的情况下,将其转换为无符号类型可能会很丑,但不一定会生成额外的代码。更大的问题是代码需要快速识别“潜在有趣”的候选项,这将花费大部分时间拒绝不感兴趣的候选项。如果让编译器自由地保留或丢弃带符号整数值的额外精度,但要求将其转换回整数类型时截断任何这样的精度,那么将启用大多数通过使溢出UB实现的有用优化... - supercat
...但是它允许需要精确包装的代码使用一个转换而不是两个(例如,(int)(x+y)>z将比较一个包装结果),并且还允许程序员在情况下编写x+y>z,其中在溢出的情况下产生0或1对于代码来说是可以接受的前提是它没有其他副作用。如果0或1都是同样可接受的结果,则允许程序员编写它,而不是编写(long)x+y>z(int)((unsigned)x+y)>z,这将允许编译器在任何给定上下文中选择哪个后者函数更便宜[每个函数在某些情况下都更便宜]。 - supercat
显示剩余4条评论

1
我敢打赌是这样的。
来自标准文档(第4页和第5页):

1.3.24 未定义行为

本国际标准没有规定的行为

[注意:当该国际标准省略任何明确的行为定义或程序使用错误的构造或错误的数据时,可以预期未定义的行为。允许的未定义行为范围从完全忽略情况并产生不可预测的结果,到在翻译或程序执行期间以环境特征的记录方式表现,有或没有发出诊断消息,到终止翻译或执行(带有发出诊断消息)。许多错误的程序结构不会引起未定义的行为;它们需要被诊断。-- 结束注释]


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