为什么这种糟糕的通用初始化语法会编译并导致不可预测的行为?
Why does this bad universal initializer syntax compile and result in unpredictable behavior?
我有一堆使用硬件(FPGA)寄存器的代码,大致是这样的形式:
struct SomeRegFields {
unsigned int lower : 16;
unsigned int upper : 16;
};
union SomeReg {
uint32_t wholeReg;
SomeRegFields fields;
};
(大多数这些寄存器类型都比较复杂。这是说明性的。)
在清理一堆以下列方式设置寄存器的代码时:
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
我有点心不在焉,不小心得到了以下结果:
SomeReg reg1{ reg1.wholeReg = 0 };
SomeReg reg2{ reg2.wholeReg = card->readReg(REG2_ADDRESS) };
当然,reg1.wholeReg =
部分是错误的,应该删除。
让我烦恼的是这个 编译 在 MSVC 和 GCC 上。我本以为这里会出现语法错误。此外,有时它工作正常并且值实际上得到 copied/assigned 正确,但其他时候,即使返回的寄存器值不是 0,它也会导致 0 值。它是不可预测的,但在哪些案例有效和哪些无效的运行之间似乎是一致的。
知道为什么编译器不将此标记为错误的语法吗?为什么它在某些情况下似乎有效但在其他情况下会中断?当然,我认为这是未定义的行为,但为什么它会 改变 看似几乎完全相同的调用之间的行为,通常是背靠背?
一些编译信息:
如果我run this through Compiler Explorer:
int main()
{
SomeReg myReg { myReg.wholeReg = 10 };
return myReg.fields.upper;
}
这是 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
标有 *
的行是此版本与没有错误 myReg.wholeReg =
部分的版本之间的唯一区别。 MSVC 给出了类似的结果,尽管即使关闭优化,它似乎也在做一些事情。在这种情况下,它只是导致一个额外的分配进出寄存器,所以它仍然按预期工作,但考虑到我的意外实验结果,它不能总是在更复杂的情况下以这种方式编译,即不从编译分配-时间可扣除的价值。
reg1.wholeReg = card->readReg(REG2_ADDRESS)
这只是一个表达式。您正在将 card->readReg(REG2_ADDRESS)
的 return 值分配给 reg1.wholeReg
,然后将此表达式的结果(指代 reg1.wholeReg
的左值)用于第一个 aggregate-initialize reg2
的成员(即 reg2.wholeReg
)。之后 reg1
和 reg2
应该保持相同的值,即函数的 return 值。
在
中语法相同
SomeReg reg1{ reg1.wholeReg = 0 };
但是,这在技术上是未定义的行为,因为您不允许在初始化之前访问变量或 class 成员。实际上,我希望这通常能正常工作,将 reg1.wholeReg
初始化为 0
然后再一次。
在其自身的初始化程序中引用变量在语法上是正确的,有时可能很有用(例如,将指针传递给变量本身)。这就是没有编译错误的原因。
int main()
{
SomeReg myReg { myReg.wholeReg = 10 };
return myReg.fields.upper;
}
这有额外的未定义行为,即使您修复了初始化,因为您根本不能在 C++ 中使用联合进行类型双关。这始终是未定义的行为,尽管某些编译器可能允许它达到 C 中允许的程度。仍然,如果 wholeReg
是联合的活跃成员(意味着最后一个分配了值的成员)。
我有一堆使用硬件(FPGA)寄存器的代码,大致是这样的形式:
struct SomeRegFields {
unsigned int lower : 16;
unsigned int upper : 16;
};
union SomeReg {
uint32_t wholeReg;
SomeRegFields fields;
};
(大多数这些寄存器类型都比较复杂。这是说明性的。)
在清理一堆以下列方式设置寄存器的代码时:
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
我有点心不在焉,不小心得到了以下结果:
SomeReg reg1{ reg1.wholeReg = 0 };
SomeReg reg2{ reg2.wholeReg = card->readReg(REG2_ADDRESS) };
当然,reg1.wholeReg =
部分是错误的,应该删除。
让我烦恼的是这个 编译 在 MSVC 和 GCC 上。我本以为这里会出现语法错误。此外,有时它工作正常并且值实际上得到 copied/assigned 正确,但其他时候,即使返回的寄存器值不是 0,它也会导致 0 值。它是不可预测的,但在哪些案例有效和哪些无效的运行之间似乎是一致的。
知道为什么编译器不将此标记为错误的语法吗?为什么它在某些情况下似乎有效但在其他情况下会中断?当然,我认为这是未定义的行为,但为什么它会 改变 看似几乎完全相同的调用之间的行为,通常是背靠背?
一些编译信息:
如果我run this through Compiler Explorer:
int main()
{
SomeReg myReg { myReg.wholeReg = 10 };
return myReg.fields.upper;
}
这是 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
标有 *
的行是此版本与没有错误 myReg.wholeReg =
部分的版本之间的唯一区别。 MSVC 给出了类似的结果,尽管即使关闭优化,它似乎也在做一些事情。在这种情况下,它只是导致一个额外的分配进出寄存器,所以它仍然按预期工作,但考虑到我的意外实验结果,它不能总是在更复杂的情况下以这种方式编译,即不从编译分配-时间可扣除的价值。
reg1.wholeReg = card->readReg(REG2_ADDRESS)
这只是一个表达式。您正在将 card->readReg(REG2_ADDRESS)
的 return 值分配给 reg1.wholeReg
,然后将此表达式的结果(指代 reg1.wholeReg
的左值)用于第一个 aggregate-initialize reg2
的成员(即 reg2.wholeReg
)。之后 reg1
和 reg2
应该保持相同的值,即函数的 return 值。
在
中语法相同SomeReg reg1{ reg1.wholeReg = 0 };
但是,这在技术上是未定义的行为,因为您不允许在初始化之前访问变量或 class 成员。实际上,我希望这通常能正常工作,将 reg1.wholeReg
初始化为 0
然后再一次。
在其自身的初始化程序中引用变量在语法上是正确的,有时可能很有用(例如,将指针传递给变量本身)。这就是没有编译错误的原因。
int main()
{
SomeReg myReg { myReg.wholeReg = 10 };
return myReg.fields.upper;
}
这有额外的未定义行为,即使您修复了初始化,因为您根本不能在 C++ 中使用联合进行类型双关。这始终是未定义的行为,尽管某些编译器可能允许它达到 C 中允许的程度。仍然,如果 wholeReg
是联合的活跃成员(意味着最后一个分配了值的成员)。