编辑:这个问题不是作为讨论未定义行为的(de)优点的论坛,但这就是它的成果。在任何情况下,this thread about a hypothetical C-compiler with no undefined behavior可能会让那些认为这是一个重要主题的人感兴趣。
“未定义行为”的经典伪装例子当然是“鼻子恶魔” - 无论C和C ++标准允许什么,物理上都是不可能的。
因为C和C ++社区倾向于强调未定义行为的不可预测性以及允许编译器在遇到未定义行为时导致程序字面任何的想法,我曾假设标准对行为,以及未定义的行为没有任何限制。
但是relevant quote in the C++ standard seems to be:
[C++14: defns.undefined]:
[..] 允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行过程中以环境特征的文档化方式行事(使用或没有发出诊断消息),终止翻译或执行(发布诊断消息)。 [..]
这实际上指定了一小组可能的选项:
我认为在大多数情况下,编译器选择忽略未定义的行为;例如,当读取未初始化的内存时,可能是插入任何代码以确保一致行为的反优化。我认为陌生人类型的未定义行为(例如“time travel”)属于第二类 - 但这要求记录这些行为并“环境特征”(所以我猜鼻子恶魔是只能由地狱计算机生产?)。
我误解了这个定义吗?这些仅仅是示例是否可能构成未定义的行为,而不是一个全面的选项列表? “任何可能发生的事情”的说法仅仅意味着忽视这种情况的意外副作用吗?
编辑:两个小问题澄清:
答案 0 :(得分:76)
是的,它允许任何事情发生。这个说明只是举例。定义很清楚:
未定义的行为:本国际标准没有要求的行为。
经常出现混淆:
你应该明白“没有要求”也意味着实施 NOT 需要保持行为未定义或做一些奇怪/不确定的事情!
C ++标准完全允许实现来记录一些理智的行为并相应地表现。 1 因此,如果你的编译器声称要包含有符号的溢出,那么逻辑(健全性?)将决定欢迎您依赖该编译器上的行为 。只是不要指望另一个编译器在没有声明的情况下表现相同。
1 哎呀,它甚至可以记录一件事并做另一件事。这是愚蠢的,它可能会让你把它扔进垃圾桶 - 为什么你会相信一个编译器的文件对你来说? - 但它不符合C ++标准。
答案 1 :(得分:23)
Undefined Behavior的一个历史目的是允许某些操作可能在不同平台上具有不同的可能有用的效果。例如,在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
的计算将产生未定义的行为,并作为
结果是程序应该打印出来#嘿嘿那里!&#34;无条件地不管
输入了什么字符。
答案 2 :(得分:15)
挑剔:您没有引用标准。
这些是用于生成C ++标准草案的来源。除非C ++工作组(ISO / IEC JTC1 / SC22 / WG21)正式采用,否则不应将这些来源视为ISO出版物,也不应将其作为文件生成。
解释:根据ISO / IEC指令第2部分,注释不是normative。
文档正文中的注释和示例仅用于提供有助于理解或使用本文档的其他信息。 它们不应包含要求(“应”;见3.3.1和表H.1)或任何被认为对文件使用必不可少的信息,例如指令(命令性;见表H.1),建议(“应该”;见3.3.2和表H.2)或许可(“可能”;见表H.3)。注释可以写成事实陈述。
强调我的。仅此就排除了“综合选项列表”。然而,给出的例子确实算作“旨在帮助理解文件的附加信息”。
请记住,“鼻妖”模因不是字面意思,就像使用气球来解释宇宙膨胀如何在物理现实中保持真理一样。这是为了说明在允许做任何事情的时候讨论应该做什么的“未定义行为”是蛮干的。是的,这意味着外太空没有实际的橡皮筋。
答案 3 :(得分:11)
在每个C和C ++标准中,未定义行为的定义基本上是标准对发生的事情没有要求。
是的,这意味着任何结果都是允许的。但是没有特定的结果 required 发生,也没有任何所需的结果没有发生。如果你有一个编译器和库一致地产生一个特定行为以响应特定的未定义行为实例 - 这样的行为不是必需的,并且即使在编译器的未来错误修复版本中也可能会改变 - 并且编译器也没关系根据每个版本的C和C ++标准,它仍然是完全正确的。
如果您的主机系统具有与插入到您的鼻孔中的探针连接形式的硬件支持,那么发生未定义的行为可能会导致不良的鼻腔效应。
答案 4 :(得分:8)
我以为我只回答你的一个观点,因为其他答案很好地回答了一般性问题,但是没有解决这个问题。
“忽略这种情况 - 是的,标准继续说这将有”不可预测的结果“,但这与编译器插入代码不同(我认为这是先决条件,你知道,鼻腔魔)。“
在没有编译器插入任何代码的情况下,使用合理的编译器可以非常合理地预期会出现鼻子恶魔的情况如下:
if(!spawn_of_satan)
printf("Random debug value: %i\n", *x); // oops, null pointer deference
nasal_angels();
else
nasal_demons();
编译器,如果它可以证明* x是空指针解除引用,则作为某些优化的一部分,完全有权说“好的,所以我看到他们在这个分支中取消引用了一个空指针if。因此,作为该分支的一部分,我被允许做任何事情。因此我可以对此进行优化:“
if(!spawn_of_satan)
nasal_demons();
else
nasal_demons();
“从那里,我可以优化到这个:”
nasal_demons();
您可以看到,在适当的情况下,这种事情如何证明对优化编译器非常有用,但却会造成灾难。我确实在一段时间后看到了一些例子,其中实际上优化能够优化这种情况非常重要。当我有更多时间时,我可能会尝试将它们挖出来。
编辑:一个例子来自我对这种优化有用的情况的记忆深处,你经常检查指针是否为NULL(可能是内联辅助函数),即使在已经取消引用它之后也是如此并没有改变它。优化编译器可以看到你已经取消引用它并因此优化了所有“is NULL”检查,因为如果你已经取消引用它并且它是null,则允许发生任何事情,包括不运行“is NULL”检查。我相信类似的论点适用于其他未定义的行为。答案 5 :(得分:6)
首先,需要注意的是,不仅未定义用户程序的行为,编译器的行为 未定义。类似地,UB在运行时不会遇到,它是源代码的属性。
对于编译器编写者,“行为未定义”意味着“您不必考虑这种情况”,甚至“您可以假设没有源代码会产生这种情况”。 编译器可以有意或无意地执行任何操作,当提供UB时,仍然符合标准,所以是的,如果您授予了访问权限......
然后,并不总是能够知道程序是否具有UB。 例如:
int * ptr = calculateAddress();
int i = *ptr;
知道这是否可能是UB需要知道calculateAddress()
返回的所有可能值,这在一般情况下是不可能的(参见“Halting Problem”)。编译器有两种选择:
ptr
将始终拥有有效地址第一个选项产生快速程序,并为程序员带来避免不良影响的负担,而第二个选项产生更安全但速度更慢的代码。
C和C ++标准使这个选择开放,大多数编译器选择第一个,而Java例如强制要求第二个。
为什么行为不是实现定义的,而是未定义的?
实施定义表示(N4296,1.9§2):
抽象机的某些方面和操作在本国际标准中描述为 实现定义(例如, 的sizeof(INT) )。这些构成了抽象机器的参数。每个实施应包括描述其特征和行为的文件 方面。 这样的文档应该定义与之对应的抽象机器的实例 实施(以下简称“对应实例”)。
强调我的。换句话说:当源代码使用实现定义的功能时,编译器 - 编写者必须完整地记录 机器代码的行为。
写入随机非空无效指针是您可以在程序中执行的最不可预测的事情之一,因此这也需要降低性能的运行时检查。
在我们有MMU之前,您可以destroy hardware写错地址,非常接近鼻子恶魔; - )
答案 6 :(得分:4)
保持行为未定义的原因之一是允许编译器在优化时做出它想要的任何假设。
如果要应用优化时必须存在某些条件,并且该条件依赖于代码中的未定义行为,则编译器可能会认为它已满足,因为符合的程序不能依赖于undefined以任何方式行为。重要的是,编译器不需要在这些假设中保持一致。 (不实现定义行为的情况)
因此,假设您的代码包含一个公认的设计示例,如下所示:
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必须为true,因此查看代码时,您可以合理地假设一旦运行代码,bar必须等于1。但是因为编译器以这种方式优化,bar永远不会被设置为1.现在断言变为false并且程序终止,这是foo没有依赖于未定义行为时不会发生的行为。
现在,如果编译器看到未定义的行为,是否有可能实际插入全新的代码?如果这样做将允许它绝对优化更多。它经常发生吗?可能不是,但你永远不能保证,所以假设鼻子是可能的,这是唯一安全的方法。
答案 7 :(得分:3)
未定义的行为只是由于规范的编写者没有预见到的情况的结果。
采用红绿灯的想法。红色表示停止,黄色表示准备红色,绿色表示停止。在这个例子中,驾驶汽车的人是规范的实施。
如果绿色和红色都亮起会怎样?你停下来,然后去?你等到红色关闭它只是绿色吗?这是规范没有描述的情况,因此,驱动程序所做的任何事情都是未定义的行为。有些人会做一件事,有些人会做。由于无法保证会发生什么,因此您希望避免这种情况。这同样适用于代码。
答案 8 :(得分:1)
未定义的行为允许编译器在某些情况下生成更快的代码。考虑两种不同的处理器架构: 处理器A在溢出时固有地丢弃进位,而处理器B则产生错误。 (当然,处理器C固有地产生鼻腔恶魔 - 它只是在鼻涕驱动的纳米机器人中释放额外能量的最简单方法......)
如果标准要求生成错误,那么为处理器A编译的所有代码基本上都将被强制包含附加指令,以执行某种溢出检查,如果是,则生成错误。这会导致代码变慢,即使开发人员知道他们最终只会添加少量数字。
未定义的行为会牺牲速度的可移植性。通过允许任何事情&#39;要发生这种情况,编译器可以避免为永远不会发生的情况编写安全检查。 (或者,你知道......他们可能。)
此外,当程序员确切地知道未定义的行为在他们给定的环境中实际会导致什么时,他们可以自由地利用这些知识来获得额外的性能。
如果您希望确保您的代码在所有平台上的行为完全相同,则需要确保没有未定义的行为&#39;曾经发生过 - 但是,这可能不是你的目标。
修改(响应OP编辑) 实施定义的行为需要一致生成鼻子恶魔。未定义的行为允许零星生成鼻子恶魔。
显示未定义行为优于实现特定行为的优势。考虑可能需要额外的代码来避免特定系统上的不一致行为。在这些情况下,未定义的行为允许更快的速度。