uint32_t和uint8_t的联合未定义的行为?
在这个答案的评论中,据说使用如下的联合将整数分割成它们的字节将是未定义的行为。 在那个地方给出的代码是相似的,虽然与此不相同,如果我更改了代码的未定义行为相关方面,请注意。
union addr { uint8_t addr8[4]; uint32_t addr32; };
到目前为止,我认为这将是一个很好的方法来做像addr = {127, 0, 0, 1};
并得到相应的uint32_t
作为回报。 (我承认根据我的系统的字节顺序,这可能产生不同的结果。但问题仍然存在。)
这是未定义的行为吗? 如果是这样,为什么? (我不知道C ++中的UB是什么意思是访问非活动的联盟成员。 )
C99
- 在这一点上,C99显然非常接近C ++ 03。
C ++ 03
- 在联合中,最多一个数据成员可以在任何时间处于活动状态,也就是说,最多一个数据成员的值可以随时存储在并集中。 C ++ 03,第9.5(1)节,第162页
然而
- 如果POD-union包含几个共享一个共同初始序列的POD结构,则允许检查任何POD结构成员的公共初始序列同上。
- 如果两个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页
我不知道什么意思是什么意思C ++中的UB是访问非活动的联盟成员。
基本上它意味着你可以在不调用未定义行为的情况下从联合中读取的唯一成员是最后写入的成员。 换句话说,如果你写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
的字节顺序。 它可能是big-endian,little-endian或middle-endian。 如果你写addr8
,然后从addr32
读取,C ++不能保证你会得到正确的值,因为在这种情况下的字节顺序。 一台计算机,它可能是一个值,而在另一台计算机上,它可能是一个不同的值。 因此,这样做(即写入一个成员并阅读另一个成员)是未定义的行为。
编辑:对于那些想知道“活动”意味着什么的人, 联盟的MSDN文档说明:
union的活动成员是其值最近设置的成员,并且只有该成员具有有效值。
编辑编辑:我一直认为这样做的行为是未定义的,但现在我不太确定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
的exception,因此以下内容不会导致相同类型的违规:
union example1 { char byte_view[sizeof(double)]; double value; };
据我所知,标准中没有任何内容留下以下代码具有未定义的行为:
example1 e; e.value = 10.0; std::out << e.byte_view[0];