为什么这个糟糕的通用初始化语法会编译并导致不可预测的行为?

Blo*_*ain 2 c++ compilation initializer-list

我有一堆使用硬件(FPGA)寄存器的代码,其大致形式如下:

struct SomeRegFields {
    unsigned int lower : 16;
    unsigned int upper : 16;
};

union SomeReg {
    uint32_t wholeReg;
    SomeRegFields fields;
};
Run Code Online (Sandbox Code Playgroud)

(这些寄存器类型中的大多数都比较复杂。这是说明性的。)

在清理一堆通过以下方式设置寄存器的代码时:

SomeReg reg1;
reg1.wholeReg = 0;
// ... assign individual fields
card->writeReg(REG1_ADDRESS, reg1.wholeReg);

SomeReg reg2;
reg2.wholeReg = card->readReg(REG2_ADDRESS);
// ... do something with reg2 field values
Run Code Online (Sandbox Code Playgroud)

我有点心不在焉,不小心得到了以下结果:

SomeReg reg1{ reg1.wholeReg = 0 };
SomeReg reg2{ reg2.wholeReg = card->readReg(REG2_ADDRESS) };
Run Code Online (Sandbox Code Playgroud)

当然,该reg1.wholeReg =部分是错误的,应该删除。

让我烦恼的是它可以在 MSVC 和 GCC 上编译。我预计这里会出现语法错误。此外,有时它工作正常,并且值实际上被正确复制/分配,但其他时候,即使返回的寄存器值非 0,它也会导致 0 值。它是不可预测的,但在哪些情况有效和哪些情况无效的运行之间似乎是一致的。

知道为什么编译器不将其标记为错误语法,以及为什么它在某些情况下似乎有效但在其他情况下会中断?当然,我认为这是未定义的行为,但为什么它会改变通常看起来几乎相同的调用(通常是背靠背)之间的行为?


一些编译信息:

如果我通过编译器资源管理器运行它

int main()
{
    SomeReg myReg { myReg.wholeReg = 10 };
    return myReg.fields.upper;
}
Run Code Online (Sandbox Code Playgroud)

这是 GCC trunk 在优化关闭 ( ) 的情况下为 main 吐出的代码-O0

main:
    push    rbp
    mov     rbp, rsp
    mov     DWORD PTR [rbp-4], 10
*   mov     eax, DWORD PTR [rbp-4]
*   mov     DWORD PTR [rbp-4], eax
    movzx   eax, WORD PTR [rbp-2]
    movzx   eax, ax
    pop     rbp
    ret
Run Code Online (Sandbox Code Playgroud)

标有 的行*是此版本与没有坏部分的版本之间的唯一区别myReg.wholeReg =。MSVC 给出了类似的结果,尽管即使关闭优化,它似乎也做了一些工作。在这种情况下,它只会导致寄存器进出的额外分配,因此它仍然按预期工作,但考虑到我的意外实验结果,在更复杂的情况下,它不能总是以这种方式编译,即不从编译分配-时间扣除价值。

use*_*522 6

reg1.wholeReg = card->readReg(REG2_ADDRESS) 
Run Code Online (Sandbox Code Playgroud)

这被简单地视为一个表达式。card->readReg(REG2_ADDRESS)您正在分配to的返回值reg1.wholeReg,然后使用该表达式的结果(引用 的左值reg1.wholeReg)来聚合初始化reg2(ie reg2.wholeReg) 的第一个成员。之后reg1reg2应该保持相同的值,即函数的返回值。

从语法上讲,同样的情况发生在

SomeReg reg1{ reg1.wholeReg = 0 };
Run Code Online (Sandbox Code Playgroud)

但是,从技术上讲,这里是未定义的行为,因为在变量或类成员初始化之前不允许访问它们。实际上,我希望这通常可以正常工作,初始化reg1.wholeReg然后0再次初始化。

在其自己的初始值设定项中引用变量在语法上是正确的,并且有时可能很有用(例如,将指针传递给变量本身)。这就是为什么没有编译错误的原因。


int main()
{
    SomeReg myReg { myReg.wholeReg = 10 };
    return myReg.fields.upper;
}
Run Code Online (Sandbox Code Playgroud)

即使您修复了初始化,这也会产生额外的未定义行为,因为您根本无法在 C++ 中使用联合进行类型双关。这始终是未定义的行为,尽管某些编译器可能允许它达到 C 中允许的程度。尽管如此,标准不允许读取fields.upperifwholeReg是联合的活动成员(意味着分配了值的最后一个成员)。