我正在阅读有关未定义行为的内容,我不确定它是否只是一个编译时功能,或者它是否可以在执行时发生。
我很好理解这个例子(这是从Undefined Behavior page of Wikipedia中提取的):
C语言的一个例子:
int foo(unsigned x) { int value = 5; value += x; if (value < 5) bar(); return value; }
x
的值不能为负,并且鉴于有符号整数溢出是C中未定义的行为,编译器可以假设在if检查value >= 5
的行。因此,if
和函数bar
的调用可以被编译器忽略,因为if
没有副作用,并且它的条件永远不会被满足。因此,上面的代码在语义上等同于:int foo(unsigned x) { int value = 5; value += x; return value; }
但这发生在编译时。
如果我写的话,例如:
void foo(int x) {
if (x + 150 < 5)
bar();
}
int main() {
int x;
std::cin >> x;
foo(x);
}
然后用户输入MAX_INT - 100
(“2147483547”,如果是32位整数)。
会有一个整数溢出,但是AFAIK,它是CPU的算术逻辑单元会产生溢出,因此这里不涉及编译器。
它仍然是未定义的行为吗?
如果是,编译器如何检测溢出?
我能想到的最好的是CPU的溢出标志。如果是这种情况,是否意味着如果在执行时随时设置CPU的溢出标志,编译器可以做任何他想做的事情?
答案 0 :(得分:6)
是的,但不一定是我认为你可能意味着它的方式,也就是说,如果在机器代码中有一个添加,并且在运行时添加包装(或以其他方式溢出,但在大多数架构上它会包装)不是UB本身。 UB完全属于C(或C ++)领域。这个添加可能是添加无符号整数或者是编译器可以进行的某种优化,因为它知道目标平台的语义并且可以安全地使用依赖包装的优化(但你不能,除非您使用无符号类型执行此操作。)
当然,这根本不意味着使用“仅在运行时换行”的构造是安全的,因为这些代码路径在编译时也会中毒。例如,在您的示例中,
extern void bar(void);
void foo(int x) {
if (x + 150 < 5)
bar();
}
由GCC 6.3编译,目标是x64到
foo:
cmp edi, -145
jl .L4
ret
.L4:
jmp bar
相当于
void foo(int x) {
if (x < -145)
bar(); // with tail call optimization
}
..如果您认为有符号整数溢出是不可能的(在某种意义上它将输入的隐式前提条件设置为不会发生溢出),这是相同的。
答案 1 :(得分:2)
您对第一个示例的分析不正确。 value += x;
等效于:
value = value + x;
在这种情况下,value
是int
,而x
是unsigned
,因此通常的算术转换意味着value
是首先转换为无符号,所以我们有一个无符号加法,根据定义,它不会溢出(它按照模块化算法具有明确定义的语义)。
将未签名的结果分配回value
时,如果结果大于INT_MAX
,则这是超出范围的分配,具有实现定义的行为。这不是溢出,因为它是赋值,而不是算术运算。
因此,哪种优化可能取决于实现如何定义整数的超出范围分配的行为。现代系统都采用具有相同的2补码表示法的值,但是历史上其他系统却做了一些不同的事情。
因此,原始示例在任何情况下都不会具有未定义的行为,建议的优化对大多数系统来说是不可能的。
您的第二个示例与您的第一个示例无关,因为它不涉及任何无符号算术。如果x > INT_MAX - 150
,则表达式x + 150
由于有符号整数溢出而导致未定义的行为。语言定义没有提及ALU或CPU,因此我们可以确定那些事情与行为是否未定义无关。
如果是,编译器如何检测到溢出?
不是必须的。正是由于 行为未定义,这意味着编译器不必担心发生溢出时会发生什么情况。它仅需发出一个可执行文件,该可执行文件例示了所定义案例的行为。
在此程序中,这些是[[INT_MIN
,INT_MAX-150
]范围内的输入,因此编译器可以将比较转换为x < -145
,因为对于所有输入都具有相同的行为。定义明确的范围,对于未定义的情况都没有关系。