访问非活动工会成员和未定义行为?

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

那么,是未定义行为吗?

25496 次浏览

C + + 11标准是这样说的

9.5工会

在联合中,最多一个非静态数据成员可以在任何时候处于活动状态,也就是说,最多一个非静态数据成员的值可以在任何时候存储在一个联合中。

如果只存储了一个值,那么如何读取另一个值呢。


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

  • 使用不同类型的成员(C906.3.2.3)访问联合对象的成员。

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

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


2016年01月05日: 通过评论,我与 C99缺陷报告 # 283建立了联系,C99缺陷报告 # 283在 C 标准文件中增加了一个类似的脚注:

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

不知道它是否澄清了很多,但是,考虑到脚注不是标准的规范。

我认为这个标准最接近于说它是未定义行为的是它在哪里定义了一个包含公共初始序列的联合的行为(C99,6.5.2.3/5) :

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

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

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

虽然两者都没有直接说明,但这两者都有一个强烈的含义,即“检查”(读取)一个成员是“允许的”只有,如果1)它是(部分)最近编写的成员,或2)是一个共同的初始序列的一部分。

这并不能直接说明不这么做是没有未定义行为的,但这是我所知道的最接近的一种说法。

现有答案中还没有提到的是第6.2.5节第21段的脚注37:

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

这个要求似乎清楚地暗示您不能写入一个成员而读入另一个成员。在这种情况下,未定义行为可能因缺乏具体说明而受到影响。

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

6.5.2.3组织架构及工会成员

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

C + + 的情况:

9.5工会〔 class.union 〕

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

C + + 后来的语言允许使用包含具有公共初始序列的 struct的联合,但是不允许类型双关。

为了确定在 C + + 中是否允许联合类型双关 ,我们必须进一步搜索。回想一下,是 C + + 11的标准参考(C99的语言与 C11类似,允许联合类型双关) :

3.9类型[ basic.type ]

类型 T 的对象的对象表示形式是由 类型 T 的对象,其中 N 等于 sizeof (T)。一个对象的值表示是一组位 保存类型 T 的值对于普通的可复制类型,值表示是对象中的一组位 表示形式,该表示形式确定一个值,该值是实现定义的 值 < sup > 42
目的是使 C + + 的内存模型与 ISO/IEC 9899编程语言 C 的内存模型兼容。

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

3.8对象生存期[ basic.life ]

T 类型对象的生存期从以下时间开始: ーー获得适合于 T 型的对齐和大小的存储器,并且 ー如果对象具有非平凡的初始化,则其初始化完成。

因此,对于联合中包含的原语类型(当然具有简单的初始化) ,对象的生命周期至少包含联合本身的生命周期。这样我们就可以

3.9.2复合类型[基本. 复合]

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

假设我们感兴趣的操作是类型双关,即获取一个非活动联合成员的值,并根据上面的内容给出对该成员引用的对象的有效引用,这个操作就是 lvalue-to-rvalue 转换:

4.1 Lvalue-to-rvalue 转换[ conv.lval ]

非函数、非数组类型 T的 glvalue 可以转换为 prvalue。 如果 T是一个不完整的类型,那么需要进行此转换的程序就是格式不正确的。如果 glvalue 所指的对象不是 T类型的对象,也不是从 T类型派生的对象,或者如果该对象未初始化,则需要进行这种转换的程序将失去未定义行为。

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

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

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

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

我会用一个例子来解释这一点。
假设我们有以下工会:

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

我假设 sizeof(int)给出4,而 sizeof(short)给出2。
在编写 union A a = {10}时,创建一个类型为 A 的新变量,将值10放入其中。

你的记忆应该是这样的: (记住所有的工会成员得到相同的位置)

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

可以看到,a.x 的值是10,a.y1的值是10,a.y [0]的值是0。

如果我这么做会怎么样?

a.y[0] = 37;

我们的记忆是这样的:

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

这将把 a.x 的值转换为2424842(十进制)。

现在,如果你的联合有一个浮点数或者双精度数,那么你的内存映射将会更加混乱,因为你存储精确数字的方式。 你可以在 给你中获得更多的信息。