有符号整数溢出是未定义行为还是实现定义行为?

3
#include <limits.h>

int main(){
 int a = UINT_MAX; 
 return 0;
}

这是UB还是实现定义?

链接表明是UB:

https://www.gnu.org/software/autoconf/manual/autoconf-2.63/html_node/Integer-Overflow-Basics

允许C/C++中的有符号整数溢出

链接表明是实现定义:

http://www.enseignement.polytechnique.fr/informatique/INF478/docs/Cpp/en/c/language/signed_and_unsigned_integers.html

转换规则指出:

否则,新类型是带符号的,并且该值无法在其中表示;结果要么是实现定义的,要么会引发实现定义的信号。

我们不是将最大的无符号值转换为有符号值吗?

我所见过的方式是gcc只截断结果。


2
你必须保持表达式中的转换溢出是正确的。在这里,当将UINT_MAX转换为int类型进行赋值时,会发生溢出。这是IDB。当溢出实际上在计算表达式时发生,例如999999999*999999999,那么就是UB了。 - Nate Eldredge
我不明白它们的区别?在这里,您还将999999999*999999999的结果转换为int类型,这实际上是将其截断。 - Dan
这不是 C 的定义方式。执行 999999999*999999999 不是按照“进行数学运算,然后根据通常的转换规则执行转换”的定义方式。它的定义是“在概念上进行数学运算,但如果结果不能用类型 int 表示,则 UB”。转换规则从未发挥作用。 - Nate Eldredge
1
如果有助于理解,long long int a = 999999999*999999999;也是未定义行为。当*运算符被评估时,未定义行为就会发生。你尝试对结果做什么并不相关。 - Nate Eldredge
4个回答

8

这两个参考都是正确的,但它们并没有解决相同的问题。

int a = UINT_MAX; 不是有符号整数溢出的实例,此定义涉及将 unsigned int 转换为 int,其值超出了类型 int 的范围。引用自 École polytechnique 网站,C标准将其行为定义为实现定义。

#include <limits.h>

int main(){
    int a = UINT_MAX;    // implementation defined behavior
    int b = INT_MAX + 1; // undefined behavior
    return 0;
}

这是C标准的文本:

6.3.1.3 有符号和无符号整数

  1. 当将整数类型的值转换为除了 _Bool 以外的另一种整数类型时,如果该值可以用新类型表示,则它保持不变。

  2. 否则,如果新类型是无符号的,则通过重复添加或减去比新类型中可以表示的最大值多一的值,直到该值位于新类型的范围内来进行转换。

  3. 否则,如果新类型是有符号的且该值不能被表示,则结果是实现定义的,或者会引发一种实现定义的信号。

一些编译器有一个命令行选项,可以将有符号算术溢出的行为从未定义行为更改为实现定义: gccclang 支持 -fwrapv,强制执行计算机的整数运算模232 或264,具体取决于有符号类型。 这样做可以防止一些有用的优化,但还可以防止一些可能破坏看似无害的代码的反直觉的优化。 有关一些示例,请参见此问题:What does -fwrapv do?


为什么有符号整数溢出是未定义行为(因此 if (i+1 < i) 可以优化为 if (0) 然后被删除),而不是实现定义的行为(因此行为根据硬件)? - pmor
@pmor:有符号整数溢出被规定为具有未定义行为,因为标准C委员会决定如此。优化编译器的编写者利用这一点来实现各种优化。 - chqrlie
你知道C委员会为什么作出这样的决定吗?可能是为了让编译器更好地优化,从而提高性能。 - pmor

2

int a = UINT_MAX; 不会溢出,因为在声明或表达式求值期间没有发生任何异常情况。该代码被“定义”为将UINT_MAX转换为类型int以初始化a,并且根据C 2018 6.3.1.3规则定义了转换。

简要来说,适用的规则如下:

  • 6.7.9 11 表示初始化类似于简单赋值:“……对象的初始值是表达式(转换后)的值;适用于简单赋值的相同类型约束和转换也适用于初始化,…”
  • 6.5.16.1 2 表示简单赋值执行转换:“在简单赋值=)中,右操作数的值转换为赋值表达式的类型,并替换左操作数指定的对象中存储的值。”
  • 6.3.1.3 3,涵盖当操作数值不能表示为带符号整数类型时的转换,表示:“结果是实现定义的,或者引发实现定义的信号。”

因此,行为已被定义。

在2018年6.5 5中有一个关于在计算表达式时发生异常情况的一般规则:

如果在求值表达式期间发生异常情况(即,如果结果在数学上未定义或不在其类型的可表示值范围内),则行为是未定义的。

然而,在上面的链中从未应用这个规则。在进行评估时,包括初始化的暗示赋值在内,我们从未得到超出其类型范围的结果。转换的输入超出了目标类型int的范围,但是转换的结果在范围内,因此没有超出范围的结果来触发异常条件。

(这可能有一个例外,即C实现可以将转换的结果定义为超出int的范围。我不知道有任何实现这样做,这可能不是6.3.1.3 3所意图的。)


1
这不是有符号整数溢出:
int a = UINT_MAX; 

这是从无符号整数类型到有符号整数类型的转换,是实现定义的。这在C标准的第6.3.1.3节中涵盖了有关有符号和无符号整数类型转换的内容:
当具有整数类型的值转换为除_Bool之外的另一种整数类型时,如果该值可以由新类型表示,则保持不变。
否则,如果新类型为无符号类型,则通过反复添加或减去比新类型中可表示的最大值多一个以上的值,直到该值在新类型的范围内进行转换。
否则,新类型为有符号类型且该值无法在其中表示;结果要么是实现定义的,要么会引发实现定义的信号。
有符号整数溢出的示例是:
int x = INT_MAX;
x = x + 1;

这是未定义的。事实上,C标准中定义未定义行为的第3.4.3节第4段如下:

整数溢出是未定义行为的示例

而整数溢出仅适用于有符号类型,根据6.2.5p9:

有符号整数类型的非负值范围是相应无符号整数类型的子范围,并且每种类型中相同值的表示相同。涉及无符号操作数的计算永远不会溢出,因为结果不能由结果无符号整数类型表示的结果将对比结果大1的数字取模,这是可以由结果类型表示的最大值。


0
在现有的“语言”(方言族群)中,C标准的编写描述通常会通过执行底层平台所做的任何操作来处理带符号整数溢出,将值截断为底层类型的长度(大多数平台都是这样做的),即使在其他平台上也会执行某些其他操作,或触发某种形式的信号或诊断。
在K&R的书《C程序设计语言》中,该行为被描述为“机器相关”。
虽然标准的作者们在已发表的原理文档中指出了一些情况,在这些情况下,他们希望常见平台的实现会以常见方式表现出来,但他们不想说某些操作在某些平台上具有定义行为而在其他平台上则没有。此外,将行为描述为“实现定义的”将会导致问题。考虑以下内容:
int f1(void);
int f2(int a, int b, int c);

int test(int x, int y)
{
  int test = x*y;
  if (f1())
    f2(test, x, y);
}

如果整数溢出的行为被定义为“实现定义”,那么任何可能引发信号或具有其他可观察副作用的实现都需要在调用f1()之前执行乘法,即使乘积的结果将被忽略,除非f1()返回一个非零值。将其归类为“未定义行为”可以避免这样的问题。
不幸的是,gcc将把“未定义行为”的分类解释为一种邀请,以不受常规因果定律约束的方式处理整数溢出。例如,对于下面这样的函数:
unsigned mul_mod_32768(unsigned short x, unsigned short y)
{
  return (x*y) & 0x7FFFu;
}

尝试使用大于INT_MAX/y的x值调用它可能会任意破坏周围代码的行为,即使函数的结果在任何观察方式下都不会被使用。

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