“未定义的行为”是否真的允许任何事情发生?

编辑:这个问题不是作为一个论坛来讨论未定义行为的(德)优点,但是这就是它的成果。 无论如何, 这个关于一个没有未定义行为的假想C编译器的线程可能会让那些认为这是一个重要主题的人感兴趣。


当然,“未定义的行为”这个典型的伪造的例子是“鼻恶魔” – 无论C和C ++标准允许如何,这在物理上是不可能的。

由于C和C ++社区倾向于强调未定义行为的不可预测性,以及在遇到未定义的行为时允许编译器使程序执行字面上的任何事情的想法,所以我认为标准没有任何限制在行为上,还有未定义的行为。

但是C ++标准中的相关引用似乎是 :

[C++14: defns.undefined]: [..]允许的未定义的行为范围从忽略完全不可预测的结果的情况,到在翻译或程序执行过程中以文件化的方式表现环境特征(无论是否发行诊断消息),终止翻译或执行(发布诊断消息)。 [..]

这实际上指定了一小组可能的选项:

  • 忽略情况 – 是的,标准继续说这将会有“不可预知的结果”,但这不像编译器插入代码(我认为这是鼻病毒的先决条件)。
  • 以一种有记录的方式表现环境特征 – 这实际上听起来相当温和。 (我当然没有听说过鼻病毒的案件记录。)
  • 终止翻译或执行 – 与诊断,不下。 那么所有的UB都会表现得如此完美。

我认为在大多数情况下,编译器会选择忽略未定义的行为; 例如,在读取未初始化的内存时,可能会插入任何代码以确保一致的行为,这是一种反优化。 我认为陌生人的不确定行为(比如“ 时间旅行 ”)属于第二类,但是这要求将这种行为记录下来并且“环境特征”(所以我猜鼻子恶魔只是由地狱电脑?)。

我误解了这个定义吗? 这些意图仅仅是作为什么可能构成未定义行为的例子 ,而不是一个全面的选择清单? “任何事情都可能发生”这个说法只是意味着忽视这种情况的意想不到的副作用?

编辑:澄清的两个小点:

  • 我原以为这个问题很清楚,我觉得对大多数人来说是这样,但我仍然会把它解释清楚:我知道“鼻子恶魔”是口齿不清的。
  • 请不要写(其他)答案解释UB允许特定于平台的编译器优化,除非您解释如何允许实现定义的行为不允许的优化。

是的,它允许发生任何事情。 这个说明只是举例。 定义非常清楚:

未定义的行为:本国际标准没有规定的行为。

未定义行为的历史目的之一是允许某些行为在不同平台上可能具有不同潜在有用效果的可能性。 例如,在C的初期,给出

 int i=INT_MAX; i++; printf("%d",i); 

一些编译器可以保证代码会打印一些特定的值(对于二进制补码机器来说,它通常是INT_MIN),而另一些编译器则可以保证程序在没有到达printf的情况下终止。 根据应用要求,任何行为都可能是有用的。 将行为保留为未定义,意味着程序异常终止是一个可以接受的溢出结果,但是产生看似有效但错误的输出的应用程序不可能放弃溢出检查,如果在可靠的陷阱平台上运行,应用程序如果发生溢出异常终止是不可接受的,但是产生算术错误的输出将会在没有溢出的平台上运行时放弃溢出检查。

然而,最近一些编译器作者似乎已经进入了一场比赛,看看谁能够最有效地消除任何标准所不具备的代码。 鉴于,例如…

 #include <stdio.h> int main(void) { int ch = getchar(); if (ch < 74) printf("Hey there!"); else printf("%d",ch*ch*ch*ch*ch); } 

一个超现代的编译器可能得出这样的结论:如果ch是74或者更大, ch*ch*ch*ch*ch就会产生未定义的行为,因此程序应该打印出“Hey there!”。 无论输入什么字符。

Nitpicking :你没有引用标准。

这些是用来生成C ++标准草案的来源。 除非C ++工作组(ISO / IEC JTC1 / SC22 / WG21)正式通过,否则这些来源不应被视为ISO出版物,也不应将其视为ISO出版物。

解释 :根据ISO / IEC指令第2部分,注释不是规范性的 。

文档中整合的注释和例子只能用于提供额外的信息,以帮助理解或使用文档。 它们不应包含要求(“必须”;见3.3.1和表H.1)或任何被认为是不可或缺的信息,例如说明书(必要的;参见表H.1),建议书(“应该”;参见3.3.2和表H.2)或许可(“可能”;见表H.3)。 笔记可以写成事实陈述。

强调我的。 仅此一项就排除了“全面的选择清单”。 然而,举例来说,这是“旨在帮助理解文件的附加信息”。

请记住,“鼻子恶魔”模因不是字面上的意思,就像用气球来解释宇宙膨胀是如何在物理现实中没有真理一样。 这是为了说明在允许做任何事情的时候讨论什么“未定义的行为” 该做什么是蛮荒的。 是的,这意味着在外层空间没有实际的橡皮筋。

在每个C和C ++标准中,未定义行为的定义基本上是标准对发生的事情没有要求。

是的,这意味着任何结果都是允许的。 但是没有必要发生的具体结果,也没有任何要求不发生的结果。 如果你有一个编译器和库一直产生一个特定的行为来响应未定义行为的特定实例,这种行为是不需要的,并且甚至在将来的编译器错误修复版本中也可能会改变 – 编译器依照C和C ++标准的每个版本,仍然是完全正确的。

如果您的主机系统以插入您的鼻孔的探针的连接形式提供硬件支持,那么发生未定义的行为将导致不期望的鼻腔效应是可能的。

我以为我会回答你的一个观点,因为其他答案很好地回答了一般问题,但是没有解决这个问题。

“无视情况 – 是的,标准继续说,这将有”不可预测的结果“,但这不像编译器插入代码(我认为这将是鼻恶魔的先决条件)。 “

如果没有编译器插入任何代码,一个合理的编译器就可以合理预期鼻恶魔的情况如下:

 if(!spawn_of_satan) printf("Random debug value: %i\n", *x); // oops, null pointer deference nasal_angels(); else nasal_demons(); 

一个编译器,如果它能证明* x是一个空指针解引用,作为一些优化的一部分,完全有权说“OK,所以我看到他们在if的这个分支里引用了一个空指针。因此,作为该部门的一部分,我可以做任何事情,所以我可以优化这个:“

 if(!spawn_of_satan) nasal_demons(); else nasal_demons(); 

“从那里,我可以优化到这个:”

 nasal_demons(); 

你可以看到在适当的情况下,这种事情对于优化编译器来说是非常有用的,但却会造成灾难。 在某些情况下,我确实看到了一些例子,实际上优化能够优化这种情况是非常重要的。 当我有更多的时间时,我可能会尝试挖掘它们。

编辑:一个例子,只是来自我的记忆深处的这种情况下,它是有用的优化是你经常检查指针为NULL(可能在内联的帮助函数),即使已经解除引用,而没有改变它。 优化编译器可以看到你已经对它进行了解引用,所以优化了所有的“is NULL”检查,因为如果你已经取消引用并且它是空的,任何事情都可以发生,包括不运行“is NULL”检查。 我相信类似的论点适用于其他未定义的行为。

首先,重要的是要注意,不是用户程序的行为是未定义的,而是编译器的行为 是未定义的 。 同样,在运行时不会遇到UB,它是源代码的属性。

对于编译器编写者来说,“行为不确定”的意思是“不必考虑这种情况”,甚至“你可以认为没有任何源代码会产生这种情况”。 编译器可以有意或无意地做任何事情,当提交UB时,仍然是标准兼容的,所以是的,如果你准许访问你的鼻子…

然后,并不总是可以知道一个程序是否有UB。 例:

 int * ptr = calculateAddress(); int i = *ptr; 

知道这是否可以是UB将需要知道calculateAddress()返回的所有可能的值,这在一般情况下是不可能的(请参阅“ 暂停问题 ”)。 编译器有两个选择:

  • 假设ptr将始终有一个有效的地址
  • 插入运行时检查以保证一定的行为

第一个选项产生快速的程序,并且避免了对程序员不利影响的负担,而第二个选项产生更安全但是更慢的代码。

C和C ++标准让这个选择开放,大多数编译器选择第一个,而Java则要求第二个。


为什么行为不是实现定义的,而是未定义的?

实施定义手段(N4296,1.9§2):

抽象机器的某些方面和操作在本标准中被描述为实现定义的(例如,sizeof(int))。 这些构成抽象机器的参数。 每个实施应包括描述其在这些方面的特点和行为的文件。 这样的文档应该定义与该实现相对应的抽象机器的实例 (以下称为“相应实例”)。

强调我的。 换句话说:当源代码使用实现定义的特性时,编译器 – 编写者必须精确地记录机器代码的行为。

写一个随机的非空无效指针是在程序中可以做的最不可预测的事情之一,所以这也需要性能降低的运行时检查。
在我们有MMU之前,你可以通过写错误的地址来破坏硬件 ,这个地址非常接近鼻恶魔;-)

离开行为未定义的原因之一是允许编译器在优化时做出任何想要的假设。

如果在应用优化时存在必须保持的条件,并且该条件取决于代码中的未定义行为,则编译器可能会认为符合条件,因为符合的程序不能依赖任何未定义的行为办法。 重要的是,编译器在这些假设中不需要保持一致。 (实现定义的行为不是这种情况)

所以假设你的代码包含了一个公认的人为的例子,如下所示:

 int bar = 0; int foo = (undefined behavior of some kind); if (foo) { f(); bar = 1; } if (!foo) { g(); bar = 1; } assert(1 == bar); 

编译器可以自由地假定!foo在第一个块中是真的,而foo在第二个块中是真的,从而优化整个代码块。 现在,逻辑上foo或!foo必须是真的,所以看代码,你可以合理地假设一旦你运行代码,bar必须等于1。 但是因为编译器以这种方式进行了优化,所以bar永远不会被设置为1.现在这个断言变成了错误,程序终止了,这是foo不依赖于未定义的行为时不会发生的行为。

现在,如果编译器看到未定义的行为,是否可以实际插入全新的代码? 如果这样做可以让它优化更多,绝对。 是否可能经常发生? 可能不会,但是你永远不能保证,所以假设鼻魔是可能的,这是唯一的安全方法。

未定义的行为仅仅是规范编写者没有预见到的情况的结果。

采取交通灯的想法。 红色表示停止,黄色表示准备红色,绿色表示去。 在这个例子中,驾驶汽车的人是规范的实施。

如果绿灯和红灯都点亮,会发生什么? 你停下来,然后去? 你等到红色熄灭,它只是绿色的? 这是规范没有描述的情况,因此,司机做的事情是未定义的行为。 有些人会做一件事,另一件事。 既然不能保证你会想要避免这种情况会发生什么。 这同样适用于代码。

未定义的行为允许编译器在某些情况下生成更快的代码。 考虑两种不同的处理器体系结构,其ADD有所不同:处理器A固有地丢弃溢出时的进位,而处理器B产生错误。 (当然,处理器C固有地产生鼻恶魔 – 它是在鼻涕动力的纳米机器人中释放额外能量的最简单方法…)

如果标准要求产生一个错误,那么为处理器A编译的所有代码基本上将被迫包括额外的指令,执行某种溢出检查,如果是,则产生一个错误。 这会导致代码变慢,即使开发人员知道他们只会添加小数字。

未定义的行为牺牲了可移植性的速度。 通过允许“任何事情”发生,编译器可以避免编写安全检查,以避免永远不会发生的情况。 (或者,你知道,他们可能会。)

另外,当程序员确切地知道未定义的行为在给定的环境中究竟会产生什么结果时,他们可以自由地利用这些知识来获得额外的性能。

如果您想确保您的代码在所有平台上的行为完全相同,则需要确保不会发生“未定义的行为” – 但是,这可能不是您的目标。

编辑:(在响应OPs编辑)实施定义的行为将需要一致的代鼻恶魔。 未定义的行为允许零星的鼻恶魔一代。

这就是未定义行为相对于实现特定行为的优势所在。 Consider that extra code may be needed to avoid inconsistent behavior on a particular system. In these cases, undefined behavior allows greater speed.