在给定程序中存在这样的语句是否意味着
整个程序未定义或该行为仅变得未定义
一旦控制流命中这个语句?
两者都不。第一个条件太强,第二个条件太弱。
对象访问有时是按顺序进行的,但标准描述了程序在时间之外的行为。丹维尔已经引用过:
如果任何此类执行包含未定义的操作,则此
国际标准对实施没有要求
使用该输入执行该程序(甚至不考虑
第一个未定义操作之前的操作)
这可以解释为:
如果程序的执行产生未定义的行为,则整个程序
未定义的行为。
因此,带有 UB 的 unreachable 语句不会给出程序 UB。一个可达语句(由于输入的值)永远不会达到,不会给程序提供 UB。这就是为什么你的第一个条件太强了。
现在,编译器一般无法判断什么具有 UB。因此,为了允许优化器对具有潜在 UB 的语句进行重新排序,如果定义了它们的行为,则可以重新排序,有必要允许 UB “及时返回”并在前一个序列点(或在 C 语言中)之前出错。 ++11 术语,用于 UB 影响在 UB 事物之前排序的事物)。所以你的第二个条件太弱了。
一个主要的例子是优化器依赖于严格的别名。严格别名规则的全部要点是,如果有问题的指针可能别名相同的内存,则允许编译器对无法有效重新排序的操作进行重新排序。因此,如果您使用非法别名指针,并且 UB 确实发生,那么它很容易影响 UB 语句“之前”的语句。对于抽象机来说,UB 语句还没有被执行。就实际的目标代码而言,它已经部分或全部执行。但该标准并没有尝试详细说明优化器对语句重新排序意味着什么,或者这对 UB 有何影响。它只是给予实施许可,只要它愿意,就可以出错。
您可以将其视为“UB 有一台时间机器”。
具体回答你的例子:
- 仅当读取 3 时,行为才未定义。
- 如果基本块包含肯定未定义的操作,编译器可以并且确实将代码消除为死代码。在不是基本块但所有分支都通向 UB 的情况下,它们是被允许的(我猜是这样)。此示例不是候选示例,除非
PrintToConsole(3)
以某种方式知道一定会回来。它可能会抛出异常或其他什么。
与第二个类似的例子是 gcc 选项-fdelete-null-pointer-checks
,它可以采用这样的代码(我还没有检查这个具体示例,认为它说明了总体思路):
void foo(int *p) {
if (p) *p = 3;
std::cout << *p << '\n';
}
并将其更改为:
*p = 3;
std::cout << "3\n";
为什么?因为如果p
为 null 则代码无论如何都有 UB,因此编译器可能会假设它不为 null 并进行相应优化。 Linux 内核被这个绊倒了(https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897)本质上是因为它在取消引用空指针的模式下运行isn't应该是 UB,它预计会导致内核可以处理的已定义硬件异常。当启用优化时,gcc 需要使用-fno-delete-null-pointer-checks
以提供超标准的保证。
附: “未定义行为何时发生?”这个问题的实际答案是“您计划当天出发前 10 分钟”。