uint32_t和uint8_t[4]的联合是否会导致未定义行为?

4
这个答案的评论中,有人说使用类似于联合体的方式将整数拆分成字节会导致未定义的行为。那里给出的代码与此相似,但并不完全相同,请注意我是否更改了与未定义行为相关的方面。
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-structs[...],则允许检查任何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结构体/联合体,因此上述代码确实是未定义的行为?

C++11

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

2
就此而言,我有litb的记录表明这不是UB。 - R. Martinho Fernandes
4
从技术角度来看,这是未定义行为;如果你读取了联合体成员中最后一次写入的成员之外的任何成员,那么从技术上讲就会产生未定义行为。不过,除非编译器的作者故意要使生活变得不可能,否则它应该可以正常工作。但需要注意的是,在 127.0.0.1 这个地址下,字节序可能会出现问题(在 IPv4 中,它看起来很像大端本地回环地址)。 - Jonathan Leffler
@JonathanLeffler 已确认。请假设这些变量已被匿名化! - moooeeeep
1
@R.MartinhoFernandes:这并不意味着它不是未定义行为。在联合体中,最多只有一个活动成员,但标准确定,在某些特定情况下检查与活动成员共享此类初始序列的联合体的另一个成员的共享初始序列是有效的。字符数组是否共享int的初始序列是需要讨论的问题。 - David Rodríguez - dribeas
@David:当然,但通常你可以通过char引用访问int的存储。这里有什么不同?如果我获取char成员的引用,那就没问题,但如果我直接这样做就不行了吗?简单地说,我不相信这个“活动成员”的论点。如果有人能提供一句明确的话来说明从非活动成员读取是UB,我会很高兴,因为我找不到。 - R. Martinho Fernandes
4个回答

10

我不知道什么是C++中的UB,以及如何访问非活动联合成员。

基本上这意味着,你只能读取最后一个写入的union成员,否则会导致未定义行为。换句话说,如果您写入了addr32,则只能从addr32读取,而不能从addr8读取,反之亦然。

这里还有一个示例:链接

编辑:由于对此是否为UB进行了大量讨论,因此请考虑以下(完全有效的)C++11示例;

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

您可以明确地看到,激活str并读取wstr可能是一个问题。您可以将其视为极端示例,因为您甚至需要通过进行放置new来激活成员,但规范实际上涵盖了这种情况,并且没有提到它在其他方面被视为特殊情况。


我认为不允许将 std::string 放入联合体中:C++03 9.5(1)规定,具有非平凡构造函数的类对象不能成为联合体的成员。C++11是否有所不同? - moooeeeep
1
@moooeeeep 是的,C++11草案9.5.4(手头没有最终版)实际上提到了这个确切的情况; 例如:考虑一个具有类型为M和N的非静态数据成员m和n的联合类型U的对象u。如果M具有非平凡的析构函数并且N具有非平凡的构造函数(例如,如果它们声明或继承虚函数),则可以使用析构函数和放置new运算符安全地从m切换到n的活动成员,如下所示:... - Joachim Isaksson

8
是的,这是未定义行为。C++标准第9.5.1节规定:
在联合体中,最多只能有一个非静态数据成员处于活动状态,也就是说,在联合体中,最多只能存储一个非静态数据成员的值。[注意:为了简化联合体的使用,提供了一个特殊保证:如果标准布局联合体包含多个共享公共初始序列(9.2)的标准布局结构,并且如果该标准布局联合体类型的对象包含其中一个标准布局结构,则允许检查任何标准布局结构成员的公共初始序列;参见9.2。——注释结束]
这意味着只有最近写入的成员可以被有效地读取(从其他成员读取技术上是未定义行为)。在任何时候,联合体中只能有一个成员处于活动状态。不是两个。
你可能会问为什么?考虑你的例子。C++不强制规定addr32的字节序。它可以是大端、小端或中间端。如果你写入addr8,然后从addr32读取,由于这种情况下的字节序,C++不能保证你会得到正确的值。在一台计算机上,它可能是一个值,在另一台计算机上,它可能是一个不同的值。因此,这样做(即写入一个成员并读取另一个成员)是未定义的行为。
编辑:对于那些想知道“活动”是什么意思的人,MSDN文档关于联合说明了以下内容:

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

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

然而,我认为通常情况下,在联合中读取非活动成员可以是未定义的行为(当然除了标准中的特殊注释),但我不知道它是否总是如此(即在我引用的C++标准部分之外可能有一些例外)。


1
这句话说明只有一个成员是活动的,它并没有提到任何关于读取的内容。 - R. Martinho Fernandes
除了你不能(合法地)读取一个非活动成员之外,其他都可以。 - Cornstalks
如果你发帖引用了那个说了“那个”的语录,我会点赞的。你发的那个并没有包含那句话。它甚至没有说明一个工会成员被认为是“活跃”的意思是什么。 - R. Martinho Fernandes
阅读最后一行。我已编辑我的答案以包含此内容。 - Cornstalks

5
基本上在C++中,你只能访问联合体的活动成员。
这意味着如果你设置了addr8,那么你应该只访问它,直到你设置了addr32,这样你就可以访问它以及其他成员。将一个成员设置为访问另一个成员的数据是应该导致未定义行为的。
当你设置一个成员时,它被认为是活动的,并且它将一直保持活动状态,直到另一个成员成为活动的

出于好奇,C语言是否也有类似的“活动成员”概念,还是没有这样的概念? - moooeeeep
1
@moooeeeep 共识似乎是这在C11中是明确定义的行为,而在C99中也很可能如此。请参见https://dev59.com/-2gu5IYBdhLWcg3wBym1。 - Nemo

5
坦率地说,我在标准中找不到任何关于这样做是未定义行为的提及。标准确实为union定义了“活动成员”的概念,但似乎没有将该想法用于其他任何内容,除了解释如何更改活动成员(§9.5p4),以及定义常量表达式(§5.9p2)。具体来说,它似乎没有明确提及访问活动成员或非活动成员的有效性。
据我所知,以下类似的操作可能会导致严格别名违规(Strict Aliasing Violation),从而产生未定义行为:
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];

在这种情况下,e.byte_view[0] 没有明确定义的值。在一个系统上编译和运行代码,然后在另一个系统上运行可能不会产生相同的结果。我认为值得指出这一点。 - Cornstalks
@Cornstalks 它具有“未指定的值”。这意味着实现可以自由选择该值,但是其他所有内容都必须按照编写的方式工作。与“未定义行为”不同,它意味着编译器可以订披萨。 - R. Martinho Fernandes
我刚才离开后修改了我的评论,然后在你回复之前的几秒钟内有所领悟,大部分与你最近的评论相符。我认为你提出了一个不错的观点,现在我正在重新考虑我的想法。我将继续寻找一个明确的答案。 - Cornstalks
这引出了一个问题,即如果某些行为没有明确定义,那么它是否属于未定义的行为? - moooeeeep
@moooeeeep 当然,访问成员是有定义的(我不打算费力找证据:如果没有定义,那就是一个缺陷)。标准似乎只在常量表达式中使用“活动成员”概念,而不用于其他情况。 - R. Martinho Fernandes
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接