为什么修改另一个变量引用的字段会导致意外行为?

Vad*_*nov 26 c++ gcc g++ volatile compiler-optimization

我写的这段代码乍一看很简单。它修改被引用变量引用的变量,然后返回引用的值。重现奇怪行为的简化版本如下所示:

#include <iostream>
using std::cout;

struct A {
    int a;
    int& b;

    A(int x) : a(x), b(a) {}
    A(const A& other) : a(other.a), b(a) {}
    A() : a(0), b(a) {}
};

int foo(A a) {
    a.a *= a.b;
    return a.b;
}


int main() {
    A a(3);

    cout << foo(a) << '\n';
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

但是,当它在启用优化(g++ 7.5)的情况下编译时,它会产生与未优化代码不同的输出(即 9 个没有优化 -正如预期的那样,3 个启用了优化)。

我知道volatile关键字,它可以防止编译器在存在某些副作用(例如异步执行和特定于硬件的东西)的情况下重新排序和其他优化,并且在这种情况下也有帮助。

但是,我不明白为什么在这种特殊情况下需要将引用 b 声明为 volatile ?这段代码的错误来源在哪里?

Chr*_*phe 13

关于标准,我找不到 UB 的来源。在我看来,这就像优化器的一个错误,它不会注意到a.b并且a.a都指向同一个对象:

  • 首先,foo()在副本上工作。我改成foo()引用传递,一直得到预期的结果。我怀疑引用的初始化有问题。但是提供的复制构造函数正确处理a.b.

  • 然后我怀疑一些 UB 与同一表达式中不确定排序的操作的副作用有关。但是对 lhs 的副作用在*=rhs 之后排序,因此这里也没有 UB。

  • *=语句之后添加一些日志记录使它意外地按预期工作。这看起来很奇怪:看起来像不遵守严格别名约束时遇到的常见问题,即当编译器没有意识到指向的对象被修改并优化代码时,就好像该值没有改变一样。在这种情况下,附加代码会导致重新加载正确的值并找到不同的结果并不罕见。

  • 然而这里没有别名问题,因为原始成员和对它的引用都基于相同的类型。

当你排除了不可能的,剩下的,无论多么不可能,都一定是事实。
——亚瑟·柯南·道尔爵士

在消除了 OP 代码中的错误和 UB 之后,唯一剩下的可能性就是优化器中的错误。优化器似乎没有注意到 aa 和 ab 是同一个对象,它只是重用了已经在寄存器中的 ab 的最新已知值。

  • @KorelK我不会低估你的实验结果,但我确实不同意你从中得出的结论。对“sleep”的函数调用导致错误消失的原因有很多,其中许多原因更可信,因为编译器以某种方式发出了程序集,导致存储到寄存器和内存之间出现竞争。此外,开发人员是否“通常”不会更新变量并返回对它的一些引用是无关紧要的。如果 C++ 规范允许,那么编译器必须支持它才能符合标准。 (4认同)
  • @KorelK您的答案引用了编译器错误,但它也引发了许多关于编译器如何运行(以及“合理代码”与标准的作用)以及代码的预期结果应该是什么的误解,这最终分散了读者的注意力并误导他们。例如,它引用了内存更新和寄存器相互“竞争”的概念,这很可能被误解和/或曲解。编译器错误很可能是基于优化过程中所做的无效假设的更简单的别名/重用错误。 (3认同)
  • @KorelK我认为推理是不同的:我继续根据C++语言规范消除可能的UB情况,并得出错误的结论,我可以链接到这种行为正常但不正常的情况申请。在你的答案中,你开始查看生成的代码,假设这是一个错误,而不验证是否有任何 UB 或依赖于实现的规则可以解释/允许此代码生成。 (3认同)
  • @KorelK 根据我自己的经验,我必须说,我总是惊讶于优化器的分析有多准确,以及在不能完全保证优化条件时它是多么谨慎。我从未见过任何根据语言规范不允许的“权衡”:没有编译器供应商敢冒这种风险,因为 C++ 用于生命攸关的医疗设备、核电站、电信网络、国防系统,甚至空间X。 (3认同)