union for uint32_t和uint8_t [4]未定义的行为?

时间:2012-04-22 20:40:41

标签: c++ c unions strict-aliasing type-punning

this answer的注释中,据说使用如下的联合将整数拆分为它们的字节将是未定义的行为。在那个地方给出的代码是相似的,虽然与此不相同,请注意我是否更改了代码的未定义行为相关方面。

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结构成员的公共初始序列同上。 / LI>
  • 如果两个POD-struct类型具有相同数量的非静态数据成员,则它们是布局兼容的,并且相应的非静态数据成员(按顺序)具有布局兼容类型 C ++ 03,第9.2(14)节,第157页
  • 如果T1和T2两种类型相同,则T1和T2是布局兼容类型。 C ++ 03,第3.9(11)节,第53页

结论

  • 由于uint8_t[4]uint32_t不是同一类型(我猜,一个strict aliasing thing)(加上两个都不是POD结构/联合),上面确实是UB?

C ++ 11

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

4 个答案:

答案 0 :(得分:10)

  

我不知道C ++中的UB是什么意思是访问非活动的联盟成员。

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

还有一个例子here

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

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

在这里你可以肯定地看到激活str和读取wstr可能是个问题。您可以将此视为一个极端的示例,因为您甚至必须通过执行新的位置来激活成员,但该规范实际上涵盖了这种情况,并没有提及它在其他方面被视为关于活动成员的特殊情况。

答案 1 :(得分:7)

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

  

在联合中,最多一个非静态数据成员可以随时处于活动状态,即at的值   大多数非静态数据成员可以随时存储在并集中。 [注意:一个特殊保证   是为了简化联合的使用:如果标准布局联合包含几个标准布局   共享公共初始序列(9.2)的结构,以及此标准布局联合类型的对象   包含一个标准布局结构,允许检查任何一个的公共初始序列   标准布局结构成员;见9.2。 - 结束说明]

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

你可能会问为什么?考虑你的例子。 C ++不强制addr32的字节顺序。它可能是big-endian,little-endian或middle-endian。如果您写入addr8,然后从addr32读取,则C ++无法保证您将获得正确的值,因为在这种情况下会出现字节序。一台计算机,它可能是一个值,而在另一台计算机上,它可能是一个不同的值。因此,这样做(即写入一个成员并阅读另一个成员)是未定义的行为。

修改:对于那些想知道“活跃”含义的人,the MSDN documentation on Unions说明:

  

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

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

我认为可以肯定地说,通常阅读联盟中的非活动成员可以是未定义的行为(当然,标准中的特殊注释除外),但我不知道总是是不是(也就是说,除了我引用的C ++标准部分的特别说明之外,可能还有一些例外)。

答案 2 :(得分:5)

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

这意味着,如果您设置addr8,那么您应该只访问该addr32,直到您设置{{1}},以便您可以访问它,依此类推。设置一个成员来访问另一个成员的数据是导致未定义行为的原因。

当您将某个成员设置为活动时,该成员将被视为活动,直到另一个成为活动成员。

答案 3 :(得分:5)

坦率地说,我在标准中找不到任何提及这样做是未定义的行为。该标准确实为工会定义了“活动成员”的概念,但除了解释如何更改活动成员(§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];