uint32_t和uint8_t[4]未定义行为的联合

union for uint32_t and uint8_t[4] undefined behavior?

本文关键字:未定义 uint8 uint32      更新时间:2023-10-16

在这个答案的评论中,据说使用如下所示的联合将整数拆分为它们的字节是未定义的行为。在那个地方给出的代码与此相似,尽管与此不同,如果我更改了代码中未定义的行为相关方面,请注明。

union addr {
 uint8_t addr8[4];
 uint32_t addr32;
};

到目前为止,我认为这将是一个很好的方法,可以做像addr = {127, 0, 0, 1};这样的事情并获得相应的uint32_t作为回报。(我承认这可能会产生不同的结果,具体取决于我的系统的字节序。然而,问题仍然存在。

这是未定义的行为吗?如果是这样,为什么?(我不知道UB在C++是什么意思是访问不活跃的工会成员。


C99

  • 在这一点上,C99非常接近C++03。

C++03

    在联合中,最多一个数据成员可以随时
  • 处于活动状态,也就是说,最多一个数据成员的值可以随时存储在联合中。C++03,第9.5(1)节,第162页

然而

  • 如果一个 POD 联合包含几个共享一个共同初始序列的 POD-struct 结构 [...] 允许检查任何 POD-struct 成员的共同初始序列
  • 如果两个 POD-struct [...] 类型具有相同数量的非静态数据成员,则它们与布局兼容,并且相应的非静态数据成员(按顺序)具有布局兼容的类型 C++03,第 9.2 (14) 节,第 157 页
  • 如果两个类型 T1 和 T2 是
  • 同一类型,则 T1 和 T2 是布局兼容的类型。C++03,第3.9(11)节,第53页

结论

  • 由于uint8_t[4]uint32_t不是同一类型(我猜,一个严格的混叠)(加上两者都不是 POD 结构/联合),以上确实是 UB?

C++11

  • 请注意,聚合类型不包括联合类型,因为具有联合类型的对象一次只能包含一个成员。C++11,脚注46,第42页

我不知道 UB 在C++中意味着什么,即访问不活跃的工会成员。

基本上,这意味着您可以在不调用未定义行为的情况下从联合中读取的唯一成员是最后一个写入的成员。换句话说,如果你写信给addr32,你只能从addr32读取,不能addr8,反之亦然。

此处也提供了一个示例。

编辑:由于已经有很多讨论,如果这是否是UB,请考虑以下(完全有效)C++11示例;

union olle {
    std::string str;
    std::wstring wstr;
};

在这里,您绝对可以看到激活str和读取wstr可能是一个问题。您可以将这视为一个极端的例子,因为您甚至必须通过放置新来激活成员,但规范实际上涵盖了这种情况,但没有提到它在其他方面被视为有关活动成员的特殊情况。

[编辑:阅读下面我编辑的部分,因为我现在不确定这是否是未定义的行为;但是,我将保持大部分答案不变,直到我可以进一步确认]是的,这是未定义的行为。C++标准第9.5.1节规定:

在联合中,最多一个非静态数据成员可以随时处于活动状态,即 大多数非静态数据成员可以随时存储在联合中。[ 注:一项特殊保证 是为了简化联合的使用:如果标准布局联合包含多个标准布局 共享公共初始序列 (9.2) 的结构,并且如果对象属于此标准布局联合类型 包含标准布局结构之一,允许检查任何 标准布局结构成员;请参阅 9.2.— 尾注 ]

这意味着只有最近写入的成员也可以有效地读取(从其他成员读取在技术上是未定义的行为)。在任何时候,工会中只能有一个成员处于活动状态。不是两个。

你可能会问为什么?考虑一下你的例子。C++ 不强制要求addr32的字节序。它可以是大端序、小端序或中端序。如果你写信给addr8,然后从addr32中读取,C++不能保证你会得到正确的值,因为在这种情况下是字节序。一台计算机,它可能是一个值,而在另一台计算机上,它可能是一个不同的值。因此,这样做(即写入一个成员并读取另一个成员)是未定义的行为。

编辑:对于那些想知道"活动"是什么意思的人,MSDN关于工会的文档指出:

联合的活动成员是最近设置其值的成员,并且只有该成员具有有效值。

编辑编辑:我一直认为这样做的行为是不确定的,但现在在R. Martinho Fernandes的评论和回答以及重新阅读MSDN的引文之后,我不太确定。该值当然是未指定/未定义的,但现在我不太确定行为是否是(未定义的值意味着您可能会得到不同的结果;未定义的行为意味着您的系统可能会崩溃,两者是不同的东西)。我将进一步考虑这个问题,并与我认识的其他人交谈,看看我是否能找到更明确的答案。

然而,我

确实认为可以肯定地说,一般来说,阅读工会中的不活跃成员可能是未定义的行为(当然,标准中的特殊说明除外),但我不知道它是否总是如此(即,除了我引用的C++标准部分中的特殊注释之外,可能还有一些例外)。

基本上是因为在C++中,您只能访问工会的活动成员。

这意味着,如果您设置了addr8那么您应该只访问该一个,直到您设置 addr32 ,以便您可以访问它等等。将一个成员设置为从另一个成员访问数据会导致未定义的行为。

当您设置某个成员时,它被视为活动状态,并且它将一直保持活动状态,直到另一个成员成为活动成员为止。

坦率地说,我在标准中找不到任何提及这样做是未定义的行为。该标准确实定义了联合的"活动成员"的概念,但它似乎没有使用这个想法,除了解释如何改变活动成员(§9.5p4)和定义常量表达式(§5.9p2)。具体来说,它似乎没有明确提到访问活动或非活动成员的有效性。

据我所知,如下所示的情况可能会导致严格的别名冲突,这是未定义的行为:

union example0 {
    short some_other_view[sizeof(double)/sizeof(short)];
    double value;
};

这不会导致严格的别名冲突,因为工会的一些特殊规则。如果您使用无法别名的类型访问相同的内存位置,即"正常"严格别名冲突,就会发生这种情况。

但是,由于在混叠规则方面存在char例外,因此以下内容不会导致相同类型的冲突:

union example1 {
    char byte_view[sizeof(double)];
    double value;
};

据我所知,标准中没有任何内容使以下代码具有未定义的行为:

example1 e;
e.value = 10.0;
std::out << e.byte_view[0];