【问题标题】:Undefined behavior causing time travel导致时间旅行的未定义行为
【发布时间】:2014-08-23 00:35:53
【问题描述】:

来自 msdn 博客的 this article 的一个例子让我很兴奋:

上面写着这个函数:

void unwitting(bool door_is_open)
{
    if (door_is_open) {
        walk_on_in();
    } else {
        ring_bell();

        // wait for the door to open using the fallback value
        fallback = value_or_fallback(nullptr);
        wait_for_door_to_open(fallback);
    }
}

可以优化成这个:

void unwitting(bool door_is_open)
{
    walk_on_in();
}

因为调用value_or_fallback(nullptr) 是未定义的行为(本文前面已经证明了这一点)。

现在我不明白的是:运行时只有在到达该行时才会进入未定义的行为。在运行时间进入 UB 之前,第一段的所有可观察到的效果都已解决,因此不应该在这里应用发生前/发生后的概念吗?

【问题讨论】:

  • LOL 我习惯 UB 点披萨或者给老板发辞职信,这时间旅行听起来很酷:D
  • 时间旅行是“未定义行为”的一种完全有效的形式。
  • 您的问题的答案是您引用的博客文章:如果在执行过程中的任何时候发生未定义的行为,则整个执行是未定义的,并且“任何事情都可能发生”适用于整个执行.另一个定义将阻止大多数指令在“可观察效果”中重新排序,因为大多数指令可能具有未定义的行为。
  • 该标准明确允许在 UB 下进行时间旅行。编译器使用此权限来利用原本无法获得的优化机会。
  • 标准的相关段落:1.9/5“如果任何此类执行包含未定义的操作,则本国际标准对使用该输入执行该程序的实现没有任何要求(甚至不考虑到第一个未定义操作之前的操作)。"

标签: c++ undefined-behavior


【解决方案1】:

确实,未定义的行为可能仅在运行时发生(例如,取消引用恰好为空的指针)。其他时候,一个程序可能静态地是“格式错误,不需要诊断”(例如,如果您在模板已经使用后为其添加显式特化),但效果相同:您不能从内部争论语言你的程序的行为方式。

编译器可以使用 UB 积极地“优化”代码生成。在您的情况下,编译器看到第二个分支将导致 UB(我假设这是静态已知的,即使您没有拼写出来),因此它可以进一步假设该分支从未被采用,因为这是无法区分的: 如果你确实进入了第二个分支,那么行为将是未定义的,包括你进入第一个分支的行为。因此编译器可以简单地将导致UB的整个代码路径视为死路并将其删除。

你没有办法证明有问题。

【讨论】:

  • 我认为UB也会导致编译器失败(例如如果编译器确定执行程序总是导致UB,编译器可以拒绝生成可执行文件)
  • @MattMcNabb 你知道是否有任何编译器实现了它?或者是否可以通过添加参数来开启?
  • 不知道,抱歉。我已经看到这个论点用来证明为什么编译器无法编译一些特别奇怪的程序,但它可能是编译器错误的一个特例
  • There's no way for you to prove that something is wrong. 我不确定你的意思。如果您的意思是在运行时无法检测到 UB,那么肯定有:使用 UBSan,又名-fsanitize=undefined,它检测和报告一大堆可怕的 UB 案例(或者,呃,所以我' m 由另一个程序员告诉我,他不是我,也没有编译和运行我的代码)。 @M.M 我相信在 constexpr 函数中调用可检测的 UB 时会发生这种情况,由于从 constexpr 表达式调用其结果需要在编译时进行评估
  • @underscore_d:我的意思是指 C++ 本身:程序本身不能检测到 UB 正在发生。当然,external 环境可以为所欲为,并提供分析工具。例如,人类或 Stack Overflow 可以告诉你有 UB。
【解决方案2】:

当人们试图将常识转化为规范,然后在没有常识的情况下解释规范时,就会发生这种情况。在我个人看来这是完全错误的,但这是语言标准化过程中正在做的事情。

在我个人看来,编译器不应该优化具有未定义行为的代码。但是当前的后现代编译器只是对其进行了优化。并且标准允许两者。

您提到的特定不当行为背后的逻辑是编译器对分支进行操作:如果分支中未定义某些内容,则它将整个分支标记为具有未定义的行为;如果一个分支有未定义的行为,它可以被任何东西替换。

最糟糕的是,新版本的编译器可能会破坏(并且确实会破坏)现有代码——要么不编译,要么编译成废话。而“现有代码”通常是非常大量的代码。

【讨论】:

  • 另一方面,这是让人们修复代码的好方法!
  • @KerrekSB 这是让人们修复他们的代码的好方法——错误消息会更合适。
  • “新版本的编译器可能会破坏(并且确实会破坏)现有代码”。他们只会破坏已经被破坏的代码。责任在于编写损坏代码的人,而不是编译器或标准编写者。
  • @18446744073709551615:存在。这是 Clang 的 UBSan。
  • “现在有调试时没有发现的错误”。 “现在”是什么意思?您是否想说到目前为止所有或更多的错误都是在调试时发现的?与其他优化技术相比,这种优化技术到底有什么变化?
【解决方案3】:

推理中有一个流程。

当编译器作者说:我们使用未定义的行为来优化程序时,有两种不同的解释:

  • 大多数人听到:我们识别未定义的行为并决定我们可以做任何我们想做的事 (*)
  • 编译器作者的意思是:我们假设未定义行为不会发生

因此,在你的情况下:

  • 取消引用 nullptr 是未定义的行为
  • 因此执行value_or_fallback(nullptr) 是未定义行为
  • 因此执行else 分支是未定义行为
  • 因此door_is_open 是false 是未定义的行为

由于未发生未定义行为(程序员发誓她将遵守使用条款),door_is_open 必然是true,编译器可以省略else 分支。

(*) Raymond Chen 居然这样表述我有点恼火……

【讨论】:

  • 当标准编写者对语言用户说“未定义的行为”时,他们也会说:“它不会破坏你的代码库,你会使用相同的东西,它只是说在一个平台上它可能是这个,另一个可能是那个”。然后编译器编写者过来说:“我们不在乎你写的大量代码,我们有标准和我们自己的大量代码要维护”......
  • @18446744073709551615 “在一个平台上可能是这样,而在另一个平台上可能是那样”。他们绝对不会这么说。欢迎您尝试寻找报价。
  • @n.m.那将是未指定行为的定义,对吧?
  • @qdii:未指定的行为或实现定义的行为是的。未定义的行为是完全禁止的。
  • @MatthieuM.:未定义的行为曾经意味着“禁止”的想法是奇怪的历史修正主义。 C 被设计为一种系统编程语言,这意味着它的大部分存在的原因在于实现让程序能够操作底层平台的低级方面的能力以这些平台和实现的文档而不是整个语言的文档定义的方式。调用“未定义行为”的事情大多是一些看似合理的实现可能触发的事情......
猜你喜欢
  • 1970-01-01
  • 2016-04-09
  • 2019-12-28
  • 2014-08-11
  • 2016-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多