139

我的印象是访问union除最后一组以外的成员是 UB,但我似乎找不到可靠的参考(除了声称它是 UB 但没有任何标准支持的答案)。

那么,这是未定义的行为吗?

4

5 回答 5

150

令人困惑的是,C 明确允许通过联合进行类型双关语,而 C++ ( ) 没有这样的权限。

6.5.2.3 结构和工会成员

95) 如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新对象中的对象表示6.2.6 中描述的类型(有时称为“类型双关”的过程)。这可能是一个陷阱表示。

C++的情况:

9.5 联合[class.union]

在一个union中,任何时候最多可以有一个非静态数据成员处于活动状态,即任何时候最多可以有一个非静态数据成员的值存储在一个union中。

C++ 后来的语言允许使用包含structs 和公共初始序列的联合;然而,这不允许类型双关语。

要确定C++ 中是否允许联合类型双关语,我们必须进一步搜索。回想一下,是 C++11 的规范性参考(C99 与 C11 有相似的语言,允许联合类型双关语):

3.9 类型 [basic.types]

4 - T 类型对象的对象表示是由 T 类型对象占用的 N 个 unsigned char 对象的序列,其中 N 等于 sizeof(T)。对象的值表示是保存类型 T 的值的一组位。对于普通可复制类型,值表示是对象表示中确定值的一组位,它是实现的一个离散元素-定义的一组值。42
42) 目的是 C++ 的内存模型与 ISO/IEC 9899 编程语言 C 的内存模型兼容。

当我们阅读时,它变得特别有趣

3.8 对象生命周期 [basic.life]

类型 T 的对象的生命周期开始于: — 获得类型 T 具有正确对齐和大小的存储,并且 — 如果对象具有非平凡初始化,则其初始化完成。

因此,对于包含在联合中的原始类型(事实上具有微不足道的初始化),对象的生命周期至少包含联合本身的生命周期。这允许我们调用

3.9.2 复合类型 [basic.compound]

如果一个类型 T 的对象位于地址 A,则称其值为地址 A 的类型为 cv T* 的指针指向该对象,而不管该值是如何获得的。

假设我们感兴趣的操作是类型双关语,即获取非活动联合成员的值,并且根据上述给出我们对该成员引用的对象的有效引用,则该操作是左值到-右值转换:

4.1 左值到右值的转换[conv.lval]

非函数、非数组类型的左值T可以转换为纯右值。如果T是不完整类型,则需要进行此转换的程序格式错误。如果泛左值引用的对象不是类型对象,T也不是派生自 的类型的对象T,或者如果该对象未初始化,则需要此转换的程序具有未定义的行为。

然后的问题是,作为非活动联合成员的对象是否通过存储初始化到活动联合成员。据我所知,情况并非如此,尽管如果:

  • 一个联合被复制到char数组存储中并返回 (3.9:2),或者
  • 一个联合按字节复制到另一个相同类型的联合 (3.9:3),或者
  • 符合 ISO/IEC 9899(就其定义而言)(3.9:4 注 42)的程序元素跨语言边界访问联合,然后

非活动成员对联合的访问被定义并被定义为遵循对象和值表示,没有上述插入之一的访问是未定义的行为。这对允许在这样的程序上执行的优化有影响,因为实现当然可以假设未定义的行为不会发生。

也就是说,尽管我们可以合法地为非活动的联合成员形成一个左值(这就是为什么在没有构造的情况下分配给非活动成员是可以的),但它被认为是未初始化的。

于 2012-08-16T23:41:10.043 回答
30

C++11 标准是这样说的

9.5 工会

在一个union中,任何时候最多可以有一个非静态数据成员处于活动状态,即任何时候最多可以有一个非静态数据成员的值存储在一个union中。

如果只存储一个值,如何读取另一个值?它只是不存在。


gcc 文档在实现定义的行为下列出了这一点

  • 使用不同类型的成员访问联合对象的成员 (C90 6.3.2.3)。

对象表示的相关字节被视为用于访问的类型的对象。请参阅类型双关语。这可能是一个陷阱表示。

表明这不是 C 标准所要求的。


2016-01-05:通过评论,我链接到C99 缺陷报告 #283,它在 C 标准文档中添加了类似的文本作为脚注:

78a) 如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分被重新解释为新的对象表示6.2.6 中描述的类型(有时称为“类型双关语”的过程)。这可能是一个陷阱表示。

考虑到脚注不是标准的规范性,不确定它是否澄清了很多。

于 2012-07-07T07:48:43.203 回答
19

我认为最接近标准的说法是未定义的行为是它定义了包含公共初始序列的联合的行为(C99,§6.5.2.3/5):

一个特殊的保证是为了简化联合的使用:如果一个联合包含多个共享一个公共初始序列的结构(见下文),并且如果联合对象当前包含这些结构之一,则允许检查公共它们中的任何一个的初始部分,任何地方的完整类型的联合声明都是可见的。如果对应的成员对于一个或多个初始成员的序列具有兼容的类型(并且对于位域,具有相同的宽度),则两个结构共享一个共同的初始序列。

C++11 在 §9.2/19 给出了类似的要求/许可:

如果一个标准布局联合包含两个或多个共享一个公共初始序列的标准布局结构,并且如果标准布局联合对象当前包含这些标准布局结构之一,则允许检查任何的公共初始部分其中。如果对应的成员具有布局兼容的类型,并且两个成员都不是位域或两者都是一个或多个初始成员序列的具有相同宽度的位域,则两个标准布局结构共享一个共同的初始序列。

尽管两者都没有直接说明,但它们都强烈暗示“检查”(读取)成员只有在 1)它是(部分)最近写入的成员,或 2)是共同首字母的一部分时才被“允许”顺序。

这不是直接声明不这样做是未定义的行为,但它是我所知道的最接近的。

于 2012-08-10T18:06:46.540 回答
12

可用答案​​尚未提及的是第 6.2.5 节第 21 段中的脚注 37:

请注意,聚合类型不包括联合类型,因为具有联合类型的对象一次只能包含一个成员。

这个要求似乎清楚地暗示你不能在一个成员中写入并在另一个成员中阅读。在这种情况下,由于缺乏规范,它可能是未定义的行为。

于 2012-08-16T22:00:52.497 回答
-3

我用一个例子很好地解释了这一点。
假设我们有以下联合:

union A{
   int x;
   short y[2];
};

我很好地假设sizeof(int)给出 4,并且sizeof(short)给出 2。
当你写得union A a = {10}很好时,创建一个 A 类型的新 var 并将值 10 放入其中。

你的记忆应该是这样的:(记住所有工会成员都在同一个位置)

       | x |
       | y[0] | y[1] |
       -----------------------------------------
   a-> |0000 0000|0000 0000|0000 0000|0000 1010|
       -----------------------------------------

如你所见,ax 的值为 10,ay 1的值为 10,而 ay[0] 的值为 0。

现在,如果我这样做会发生什么?

a.y[0] = 37;

我们的记忆将如下所示:

       | x |
       | y[0] | y[1] |
       -----------------------------------------
   a-> |0000 0000|0010 0101|0000 0000|0000 1010|
       -----------------------------------------

这会将 ax 的值变为 2424842(十进制)。

现在,如果你的 union 有一个浮点数或双精度数,那么你的内存映射就会更加混乱,因为你存储精确数字的方式。您可以在这里获得更多信息。

于 2012-08-17T07:00:47.880 回答