"Undefined Behavior" 真的允许*任何*发生吗?

Does "Undefined Behavior" really permit *anything* to happen?

“未定义行为”的经典杜撰示例当然是“鼻恶魔”——无论 C 和 C++ 标准允许什么,这在物理上是不可能的。

因为 C 和 C++ 社区倾向于强调未定义行为的不可预测性以及允许编译器使程序按字面意思执行 任何事情 的想法遇到未定义的行为,我曾假设标准对未定义行为的行为没有任何限制。

但是 relevant quote in the C++ standard seems to be:

[C++14: defns.undefined]: [..] Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). [..]

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

我假设在大多数情况下,编译器会选择忽略未定义的行为;例如,当读取未初始化的内存时,插入任何代码以确保一致的行为可能是一种反优化。我想奇怪的未定义行为类型(例如“time travel”)将属于第二类——但这需要记录此类行为和“环境特征”(所以我猜鼻恶魔是只能由地狱般的计算机生产?)。

我是不是误解了定义?这些是否仅作为可能构成未定义行为的示例,而不是选项的综合列表?声称“任何事情都可能发生”是否仅仅是忽视这种情况的意外副作用?

两个小的澄清点:


这个问题的目的不是作为讨论未定义行为的(缺点)优点的论坛,但它就是这样。无论如何,this thread about a hypothetical C-compiler with no undefined behavior 可能对那些认为这是一个重要话题的人有额外的兴趣。

是的,它允许任何事情发生。该说明只是举例说明。定义很清楚:

Undefined behavior: behavior for which this International Standard imposes no requirements.


易混淆点:

你应该明白 "no requirement" 意味着实施是 NOT 要求保留行为未定义或做某事bizarre/nondeterministic!

C++ 标准完全允许该实现记录一些合理的行为并相应地行为。1 因此,如果您的编译器声称在有符号溢出时回绕,逻辑(合理性?) 会指示欢迎您依赖该编译器 上的行为 。只是不要指望另一个编译器会以相同的方式运行,如果它没有声明的话。

1哎呀,它甚至允许记录一件事并做另一件事。那将是愚蠢的,它可能会让你把它扔进垃圾桶——你为什么要相信一个编译器,它的文档是骗你的?——但这并不违反 C++ 标准。

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

是的,这意味着任何结果都是允许的。但是没有需要发生的特定结果,也没有需要不会发生的任何结果。如果您有一个编译器和库能够始终如一地产生特定行为以响应未定义行为的特定实例——这种行为不是必需的,甚至可能在您的编译器的未来错误修复版本中发生变化——并且编译器根据每个版本的 C 和 C++ 标准,仍然是完全正确的。

如果您的主机系统以连接到插入鼻孔的探针的形式提供硬件支持,则发生未定义行为会导致不良鼻部影响的可能性在一定范围内。

吹毛求疵:你没有引用标准。

These are the sources used to generate drafts of the C++ standard. These sources should not be considered an ISO publication, nor should documents generated from them unless officially adopted by the C++ working group (ISO/IEC JTC1/SC22/WG21).

解释:根据ISO/IEC指令第2部分,注释不是normative

Notes and examples integrated in the text of a document shall only be used for giving additional information intended to assist the understanding or use of the document. They shall not contain requirements ("shall"; see 3.3.1 and Table H.1) or any information considered indispensable for the use of the document e.g. instructions (imperative; see Table H.1), recommendations ("should"; see 3.3.2 and Table H.2) or permission ("may"; see Table H.3). Notes may be written as a statement of fact.

强调我的。仅此一项就排除了 "comprehensive list of options"。然而,举出例子确实算作 "additional information intended to assist the understanding .. of the document"。

请记住,"nasal demon" 表情包不能按字面意思理解,就像用气球来解释宇宙膨胀的原理在物理现实中是不成立的一样。这是为了说明在允许做任何事情的情况下讨论 "undefined behavior" 应该 做什么是愚蠢的。是的,这意味着 space.

外面没有真正的橡皮筋

未定义行为的历史目的之一是允许某些操作在不同平台上可能具有不同的潜在有用效果。比如在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!" 不管 键入的是什么字符。

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

"Ignoring the situation -- Yes, the standard goes on to say that this will have "不可预测的结果”,但这与编译器插入代码不同(我认为这是鼻恶魔的先决条件)。

在没有编译器插入任何代码的情况下,可以非常合理地预期鼻恶魔会在合理的编译器中发生的情况如下:

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

一个编译器,如果它能证明 *x 是一个空指针解引用,那么作为某些优化的一部分,它完全有权说 "OK, so I see that they've dereferenced a null pointer in this branch of the if. Therefore, as part of that branch I'm allowed to do anything. So I can therefore optimise to this:"

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

"And from there, I can optimise to this:"

nasal_demons();

你可以看到这种事情在适当的情况下如何证明对优化编译器非常有用,但却会造成灾难。我确实看到了一些例子,其中实际上优化能够优化这种情况很重要。以后有时间我可能会试着把它们挖出来。

编辑:我记忆深处的一个例子是这样一种情况,它对优化很有用,你经常检查一个指针是否为 NULL(可能在内联辅助函数中),即使已经取消引用它并且没有更改它。优化编译器可以看到你已经取消引用它并因此优化所有 "is NULL" 检查,因为如果你已经取消引用它并且它为 null,则允许发生任何事情,包括不 运行 "is NULL" 检查。我相信类似的论点适用于其他未定义的行为。

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

以红绿灯为例。红色代表停止,黄色代表准备红色,绿色代表前进。在这个例子中,驾驶汽车的人是规范的实现。

如果绿色和红色都亮了会怎样?你停下来然后走吗?您是否等到红色熄灭而变成绿色?这是规范没有描述的情况,因此,驱动程序所做的任何事情都是未定义的行为。有些人会做一件事,有些人会做另一件事。由于无法保证会发生什么,因此您希望避免这种情况。这同样适用于代码。

首先,需要注意的是,不仅用户程序的行为是未定义的,编译器的行为也是未定义。同样,UB也不是运行时遇到的,是一个属性的源码。

对于编译器编写者来说,"the behaviour is undefined" 意味着 "you do not have to take this situation into account",甚至 "you can assume no source code will ever produce this situation"。 当与 UB 一起出现时,编译器可以有意或无意地做任何事情,并且仍然符合标准,所以是的,如果您允许访问您的鼻子...

那么,不是总能知道一个程序有没有UB。 示例:

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

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

  • 假设 ptr 总是有一个有效地址
  • 插入运行时检查以保证特定行为

第一个选项产生快速的程序,并将避免不良影响的负担放在程序员身上,而第二个选项产生更安全但更慢的代码。

C 和 C++ 标准保留此选择,大多数编译器选择第一个,而 Java 例如强制选择第二个。


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

实现定义 表示 (N4296, 1.9§2):

Certain aspects and operations of the abstract machine are described in this International Standard as implementation-defined (for example, sizeof(int) ). These constitute the parameters of the abstract machine. Each implementation shall include documentation describing its characteristics and behavior in these respects. Such documentation shall define the instance of the abstract machine that corresponds to that implementation (referred to as the “corresponding instance” below).

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

写入随机非空无效指针是您在程序中可以做的最不可预测的事情之一,因此这也需要降低性能的运行时检查。
在我们拥有 MMU 之前,您可以 destroy hardware 写入错误的地址,这 非常 接近鼻恶魔 ;-)

在某些情况下,未定义的行为允许编译器生成更快的代码。考虑两种 ADD 不同的处理器架构: 处理器 A 在溢出时固有地丢弃进位位,而处理器 B 产生错误。 (当然,处理器 C 本身会产生 Nasal Demons - 这是在鼻涕驱动的纳米机器人中释放额外能量的最简单方法......)

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

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

此外,当程序员确切地知道未定义的行为在他们给定的环境中实际会导致什么时,他们可以自由地利用这些知识来获得额外的性能。

如果您想确保您的代码在所有平台上的行为完全相同,您需要确保 'undefined behavior' 永远不会发生 - 然而,这可能不是您的目标。

编辑:(响应OP编辑) 实现定义的行为需要一致的鼻恶魔生成。 undefined behavior允许零星生成鼻魔

这就是未定义行为相对于实现特定行为的优势所在。考虑到可能需要额外的代码来避免特定系统上的不一致行为。在这些情况下,未定义的行为允许更快的速度。

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

如果在应用优化时存在某些必须满足的条件,并且该条件依赖于代码中未定义的行为,那么编译器可能会假定它已满足,因为符合要求的程序不能依赖于以任何方式处理未定义的行为。重要的是,编译器不需要在这些假设中保持一致。 (不是实现定义行为的情况)

假设您的代码包含一个公认的人为设计的示例,如下所示:

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。现在断言变为 false,程序终止,如果 foo 不依赖于未定义的行为,这种行为就不会发生。

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