usr*_*usr 88 c++ dead-code undefined-behavior language-lawyer unreachable-code
请考虑以下声明:
*((char*)NULL) = 0; //undefined behavior
Run Code Online (Sandbox Code Playgroud)
它明确地调用未定义的行为.在给定的程序中是否存在这样的语句意味着整个程序是未定义的,或者一旦控制流命中这个语句,该行为只会变得不确定?
如果用户从未输入数字,3
是否可以明确定义以下程序?
while (true) {
int num = ReadNumberFromConsole();
if (num == 3)
*((char*)NULL) = 0; //undefined behavior
}
Run Code Online (Sandbox Code Playgroud)
或者,无论用户输入什么,它都是完全未定义的行为?
此外,编译器是否可以假定在运行时永远不会执行未定义的行为?这样可以及时推理:
int num = ReadNumberFromConsole();
if (num == 3) {
PrintToConsole(num);
*((char*)NULL) = 0; //undefined behavior
}
Run Code Online (Sandbox Code Playgroud)
在这里,编译器可以推断,以防num == 3
我们总是调用未定义的行为.因此,这种情况必须是不可能的,并且不需要打印该号码.整个if
声明可以优化.根据标准,是否允许这种向后推理?
Ste*_*sop 63
在给定的程序中是否存在这样的语句意味着整个程序是未定义的,或者一旦控制流命中这个语句,该行为只会变得不确定?
都不是.第一个条件太强,第二个条件太弱.
对象访问有时会被排序,但标准会在时间之外描述程序的行为.丹维尔已经引用:
如果任何此类执行包含未定义的操作,则此国际标准不要求使用该输入执行该程序的实现(甚至不考虑第一个未定义操作之前的操作)
这可以解释为:
如果程序的执行产生未定义的行为,则整个程序具有未定义的行为.
因此,UB无法访问的语句不会给程序UB.从未达到(由于输入值)的可达语句,不会给出程序UB.这就是为什么你的第一个条件太强了.
现在,编译器一般不能告诉UB有什么.因此,要允许优化器重新排序与潜在UB语句将被重新订购应他们的行为被定义,有必要允许UB为"达到时光倒流"和出问题之前,前面的序列点(或C中++ 11术语,用于UB影响在UB之前排序的事物).因此,你的第二个条件太弱了.
一个主要的例子是优化器依赖于严格的别名.严格别名规则的重点是允许编译器重新排序无法有效重新排序的操作,如果有问题的指针可能是同一个内存.因此,如果您使用非法别名指针,并且确实发生UB,那么它很容易影响UB语句"之前"的语句.就抽象机而言,UB语句尚未执行.就实际的目标代码而言,它已部分或完全执行.但是标准并没有试图详细说明优化器重新排序语句意味着什么,或者它对UB有什么影响.它只是让实施许可证尽快出错.
您可以将此视为"UB有时间机器".
专门回答你的例子:
PrintToConsole(3)
以某种方式确定返回,否则此示例不是候选者.它可能抛出异常或其他什么.与你的第二个类似的例子是gcc选项-fdelete-null-pointer-checks
,它可以采用这样的代码(我没有检查过这个具体的例子,认为它说明了一般的想法):
void foo(int *p) {
if (p) *p = 3;
std::cout << *p << '\n';
}
Run Code Online (Sandbox Code Playgroud)
并将其更改为:
*p = 3;
std::cout << "3\n";
Run Code Online (Sandbox Code Playgroud)
为什么?因为if p
为null,所以代码仍然具有UB,因此编译器可能认为它不是null并且相应地进行优化.Linux内核绊倒这个(https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897),主要是因为它在取消对NULL指针的模式运行不应该如果是UB,则预计会导致内核可以处理的已定义硬件异常.启用优化后,gcc需要使用-fno-delete-null-pointer-checks
才能提供超标准保证.
PS对"未定义行为何时触发?"这个问题的实际答案?是"你计划离开这一天前10分钟".
Dan*_*vil 10
标准规定为1.9/4
[注意:本国际标准对包含未定义行为的程序的行为没有要求. - 结束说明]
有趣的是,"包含"意味着什么.稍后在1.9/5它说:
但是,如果任何此类执行包含未定义的操作,则此国际标准不要求使用该输入执行该程序的实现(甚至不考虑第一个未定义操作之前的操作)
在这里,它特别提到"执行......用那个输入".我会解释为,在一个可能的分支中未定义的行为现在不执行不会影响当前的执行分支.
然而,另一个问题是基于代码生成期间的未定义行为的假设.有关详细信息,请参阅Steve Jessop的答案.
一个有益的例子是
int foo(int x)
{
int a;
if (x)
return a;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
当前的GCC和当前的Clang都将优化(在x86上)
xorl %eax,%eax
ret
Run Code Online (Sandbox Code Playgroud)
因为他们推断出x
if (x)
控制路径中的UB 始终为零.海湾合作委员会甚至不会给你使用未初始化的价值警告!(因为应用上述逻辑的传递在生成未初始化值警告的传递之前运行)