具有未定义行为的分支可以被认为无法访问,并优化为死代码?

考虑以下陈述:

*((char*)NULL) = 0; //undefined behavior 

它清楚地调用未定义的行为。 在给定的程序中是否存在这样的陈述意味着整个程序是不确定的,或者一旦控制stream程达到这个陈述,行为就变得不明确了?

如果用户从不input数字3 ,下面的程序是否可以定义好?

 while (true) { int num = ReadNumberFromConsole(); if (num == 3) *((char*)NULL) = 0; //undefined behavior } 

或者,无论用户input什么内容,都完全是未定义的行为?

另外,编译器能否假定在运行时永远不会执行未定义的行为? 这将允许及时推理:

 int num = ReadNumberFromConsole(); if (num == 3) { PrintToConsole(num); *((char*)NULL) = 0; //undefined behavior } 

在这里,编译器可以推断,如果num == 3我们将始终调用未定义的行为。 因此,这种情况一定是不可能的,这个数字不需要打印。 整个if语句可以被优化。 按照标准是否允许这种倒退的推理?

在给定的程序中是否存在这样的陈述意味着整个程序是不确定的,或者一旦控制stream程达到这个陈述,行为就变得不明确了?

都不是。 第一个条件太强,第二个太弱。

对象访问有时是按顺序排列的,但是标准描述了程序在时间之外的行为。 丹维尔已经引用:

如果任何这样的执行包含未定义的操作,则本国际标准没有要求执行该程序的实现与该input(甚至不涉及在第一个未定义操作之前的操作)

这可以解释:

如果程序的执行产生未定义的行为,则整个程序具有未定义的行为。

所以,与UB不可达的声明不会给程序UB。 (由于input的值)的可达声明永远不会到达,不会给程序UB。 这就是为什么你的第一个条件太强大了。

现在,编译器不能一般地告诉UB有什么。 因此,为了让优化器重新排列具有潜在UB的语句,如果它们的行为被定义,那么它将是可重新订购的,有必要允许UB在前面的序列点之前(或者在C UB的术语,UB影响UB之前sorting的东西)。 所以你的第二个条件太弱了。

一个主要的例子就是优化器依赖严格的别名。 严格的别名规则的重点在于,如果有可能的指针可能是相同的内存,编译器可能会重新sorting无法重新sorting的操作。 所以如果你使用非法的别名指针,并且UB确实发生,那么它可以很容易地影响UB语句之前的语句。 就抽象机器而言,UB声明还没有被执行。 就实际的目标代码而言,它已被部分或全部执行。 但是标准并没有试图详细说明优化器重新sorting语句意味着什么,或者UB意味着什么。 它只是让实施许可证出现问题而已。

你可以把它想成“UB有时间机器”。

特别要回答你的例子:

  • 如果读取了3,行为只是未定义的。
  • 如果一个基本块包含一个确定的操作,那么编译器可以消除代码。 他们被允许(而且我猜测)在不是基本块的情况下,而是所有分支都导致UB的情况。 这个例子不是一个候选人,除非PrintToConsole(3)以某种方式知道肯定会返回。 它可以抛出一个exception或其他。

与你的第二个类似的例子是gcc选项-fdelete-null-pointer-checks ,它可以采取这样的代码(我没有检查这个特定的例子,认为它是一般的想法的说明):

 void foo(int *p) { if (p) *p = 3; std::cout << *p << '\n'; } 

并将其更改为:

 *p = 3; std::cout << "3\n"; 

为什么? 因为如果p是空的,那么代码就会有UB,所以编译器可能会认为它不为空,并相应地进行优化。 linux内核绊倒了这个( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ),主要是因为它运行在一个应该引用空指针的模式是UB,预计会导致内核可以处理的定义的硬件exception。 当启用优化时,gcc需要使用-fno-delete-null-pointer-checks来提供超越标准的保证。

PS对“未定义行为何时发生”这个问题的实际答案。 是“你计划在一天之前离开的10分钟”。

标准状态在1.9 / 4

[注:本标准对含有未定义行为的程序行为没有要求。 – 结束注意]

有趣的一点可能是“包含”的意思。 稍后在1.9 / 5它说:

然而,如果任何这样的执行包含未定义的操作,则本国际标准不要求执行该程序的实现具有该input(甚至不涉及在第一个未定义操作之前的操作)

这里特别提到“执行…与这个input”。 我将这样解释为,在一个可能的分支中未定义的行为现在不被执行,不会影响当前的分支执行。

然而,不同的问题是在代码生成期间基于未定义行为的假设。 请参阅Steve Jessop的答案,了解更多细节。

一个有启发意义的例子是

 int foo(int x) { int a; if (x) return a; return 0; } 

当前的GCC和当前的Clang都会优化这个(在x86上)

 xorl %eax,%eax ret 

因为他们推断xif (x)控制path中从UB 始终为零 。 GCC甚至不会给你一个使用未初始化值的警告! (因为应用上述逻辑的通行证在生成未初始化值警告的通行证之前运行)

目前的C ++工作草案在1.9.4中说过

这个国际标准对包含未定义行为的程序的行为没有要求。

基于此,我会说在任何执行path中包含未定义行为的程序在每次执行时都可以执行任何操作。

关于未定义的行为和编译器通常会做两件非常好的文章:

  • C和C ++中未定义行为的指南
  • 每个C程序员应该知道什么是未定义的行为

“行为”一词意味着正在做某事。 一个永远不会执行的状态pipe理员不是“行为”。

插图:

 *ptr = 0; 

那是未定义的行为? 假设我们在程序执行期间至less100%确定ptr == nullptr 。 答案应该是肯定的。

那这个呢?

  if (ptr) *ptr = 0; 

这是不明确的? (还记得ptr == nullptr至less一次吗?)我当然希望不要,否则你将不能写任何有用的程序。

在这个答案中没有srandardese伤害。

如果程序达到调用未定义行为的语句,则不要求程序的任何输出/行为; 它们是否会在未定义的行为被调用之前“之前”或“之后”发生并不重要。

你对所有三个代码片断的推理是正确的。 具体来说,编译器可以将任何无条件地调用未定义行为的语句视为GCC将__builtin_unreachable()作为优化提示,表示语句无法访问(从而无条件地导致所有代码path也无法访问)。 其他类似的优化当然是可能的。

未定义的行为会在程序导致未定义的行为时触发,而不pipe接下来发生什么。 但是,您给出了以下示例。

 int num = ReadNumberFromConsole(); if (num == 3) { PrintToConsole(num); *((char*)NULL) = 0; //undefined behavior } 

除非编译器知道PrintToConsole定义,否则不能删除if (num == 3)条件。 让我们假设你有LongAndCamelCaseStdio.h系统头文件和下面的PrintToConsole声明。

 void PrintToConsole(int); 

没有太大的帮助,好的。 现在,通过检查这个函数的实际定义,让我们看看供应商是多么的邪恶(或者不是那么邪恶,未定义的行为本可以更糟)。

 int printf(const char *, ...); void exit(int); void PrintToConsole(int num) { printf("%d\n", num); exit(0); } 

编译器实际上必须假定编译器不知道它做了什么的任意函数可能会退出或引发exception(在C ++的情况下)。 你可以注意到*((char*)NULL) = 0; 将不被执行,因为PrintToConsole调用后执行不会继续。

PrintToConsole实际返回时会PrintToConsole未定义的行为。 编译器希望这不会发生(因为这会导致程序执行未定义的行为,无论如何),因此可能会发生任何事情。

但是,让我们考虑别的。 假设我们正在进行空检查,并在空检查后使用该variables。

 int putchar(int); const char *warning; void lol_null_check(const char *pointer) { if (!pointer) { warning = "pointer is null"; } putchar(*pointer); } 

在这种情况下,很容易注意到lol_null_check需要一个非空指针。 分配给全局非易失性warningvariables不是可以退出程序或抛出任何exception的东西。 pointer也是非易失性的,所以它不能在函数中间奇迹般地改变它的值(如果是的话,这是不确定的行为)。 调用lol_null_check(NULL)会导致未定义的行为,这可能会导致variables不被分配(因为此时程序执行未定义行为的事实是已知的)。

但是,未定义的行为意味着程序可以做任何事情。 因此,没有什么能阻止未定义的行为,并在执行第一行int main()之前崩溃程序。 这是不确定的行为,它没有任何意义。 在input3之后,它可能会崩溃,但未定义的行为将会及时回溯,甚至在input3之前崩溃。谁知道,可能是未定义的行为会覆盖您的系统RAM,导致系统在2周后崩溃,而你的未定义程序没有运行。

很多标准对于许多types的东西花费了大量的努力来描述实现应该或不应该做的事情,使用类似于IETF RFC 2119中定义的命名法(尽pipe不一定引用该文档中的定义)。 在许多情况下, 除了 所有符合实现必须符合的要求之外,对实现应该做的事情的描述( 除非它们是无用的或不切实际的)

不幸的是,C和C ++标准倾向于避开对事物的描述,尽pipe这些描述并非100%要求,但仍然期望质量实现不会产生相反的行为。 一个build议,实现应该做的事情可能被认为是暗示那些不低劣的,并且在一般情况下显而易见哪些行为是有用的或实际的,相对于不切实际和无用的情况下,对于给定的实现,很less有人认为需要标准来干预这种判断。

一个聪明的编译器可以符合标准,同时消除任何代码,除非代码接收到不可避免会导致未定义行为的input,否则不起作用,但“聪明”和“哑”不是反义词。 标准作者认为可能存在某种在特定情况下行为有用的实现将是无用和不切实际的,并不意味着对这种行为是否应该被认为是实际的和对他人有用的判断。 如果一个实施可以在没有“死枝”修剪机会的情况下维护一个没有成本的行为保证,那么几乎任何用户代码可以从这个保证中得到的价值将超过提供它的成本。 在不需要放弃任何东西的情况下,死支消除可能是正确的,但是如果在给定情况下,用户代码几乎可以处理除了死区消除以外的任何可能的行为,用户代码将不得不花费的任何努力避免UB可能会超过DBE的价值。