这种编译器优化不一致是否完全由未定义的行为解释?

Is this compiler optimization inconsistency entirely explained by undefined behaviour?

在前几天与几位同事的讨论中,我用 C++ 拼凑了一段代码来说明内存访问冲突。

在长时间几乎完全使用具有垃圾收集功能的语言之后,我目前正在慢慢回归 C++,而且我想,我失去了触摸显示,因为我对我的行为感到非常困惑短节目展出

有问题的代码是这样的:

#include <iostream>

using std::cout;
using std::endl;

struct A
{
    int value;
};

void f()
{
    A* pa;    // Uninitialized pointer
    cout<< pa << endl;
    pa->value = 42;    // Writing via an uninitialized pointer
}

int main(int argc, char** argv)
{   
    f();

    cout<< "Returned to main()" << endl;
    return 0;
}

我在 Ubuntu 15.04 上用 GCC 4.9.2 编译了它,并设置了 -O2 编译器标志。当 运行 时,我的期望是当我的注释表示为 "writing via an uninitialized pointer" 的行被执行时它会崩溃。

然而,出乎我意料的是,程序运行成功地结束了,产生了以下输出:

0
Returned to main()

我用 -O0 标志重新编译了代码(以禁用所有优化)并再次 运行 程序。这一次,行为如我所料:

0
Segmentation fault

(好吧,差不多:我没想到一个指针会被初始化为0。)基于这个观察,我推测在用-O2编译时设置,致命指令被优化掉。这是有道理的,因为在有问题的行设置 pa->value 之后,没有进一步的代码访问它,因此,据推测,编译器确定它的删除不会修改程序的可观察行为。

我重复了几次,每次程序在没有优化的情况下编译时都会崩溃,而在使用 -O2.

编译时奇迹般地工作

当我添加一行输出 pa->valuef() 的主体末尾时,我的假设得到进一步证实:

cout<< pa->value << endl;

正如预期的那样,有了这一行,程序总是崩溃,无论编译它的优化级别如何。

如果到目前为止我的假设是正确的,这一切都是有道理的。 但是,如果我将代码从 f() 的主体直接移动到 main(),我的理解就会有所不同,就像这样:

int main(int argc, char** argv)
{   
    A* pa;
    cout<< pa << endl;
    pa->value = 42;
    cout<< pa->value << endl;

    return 0;
}

禁用优化后,此程序如预期的那样崩溃。然而,使用 -O2,程序成功运行到最后并产生以下输出:

0
42

这对我来说毫无意义。

This answer 提到 "dereferencing a pointer that has not yet been definitely initialized",这正是我正在做的,作为 C++ 中未定义行为的来源之一。

那么,与 f() 中的代码相比,优化对 main() 中代码的影响方式是否存在差异,完全可以通过我的程序包含 UB 的事实来解释,因此编译器是"go nuts" 在技术上是免费的,或者与其他例程中的代码相比,main() 中的代码优化方式之间是否存在一些我不知道的根本差异?

您的程序有未定义的行为。这意味着任何事情都可能发生。该程序根本不包含在 C++ 标准中。您不应该抱有任何期望。

人们常说未定义的行为可能 "launch missiles" 或 "cause demons to fly out of your nose",以强化这一点。后者比较牵强但是前者是可行的,想象一下你的代码在一个核发射场上,野指针恰好写了一段启动global thermouclear的内存war..

写入未知指针一直是可能产生未知后果的事情。更糟糕的是当前流行的哲学,它建议编译器应该假设程序永远不会接收导致 UB 的输入,因此应该优化任何代码来测试这些输入,如果这样的测试不会阻止 UB 的发生。

因此,例如,给定:

uint32_t hey(uint16_t x, uint16_t y)
{
  if (x < 60000)
    launch_missiles();
  else
    return x*y;
}
void wow(uint16_t x)
{
  return hey(x,40000);
}

32 位编译器可以合法地用无条件调用替换 wow launch_missiles 不考虑 x 的值,因为 x "can't possibly" 大于 53687(任何超出该值的值都会导致 x*y 的计算溢出。即使C89 的作者指出,那个时代的大多数编译器在上述情况下都会计算出正确的结果,因为标准没有对编译器施加任何要求,超现代哲学将其视为 "more efficient"编译器假设程序永远不会收到需要依赖此类东西的输入。