提问人:Luchian Grigore 提问时间:7/7/2012 最后编辑:Aykhan HagverdiliLuchian Grigore 更新时间:3/25/2020 访问量:28458
访问不活跃的工会成员和未定义的行为?
Accessing inactive union member and undefined behavior?
问:
我的印象是访问最后一个集合以外的成员是 UB,但我似乎找不到可靠的参考(除了声称它是 UB 但没有标准支持的任何答案)。union
那么,这是未定义的行为吗?
答:
C++11标准是这样说的
9.5 工会
在联合中,任何时候最多可以有一个非静态数据成员处于活动状态,也就是说,一个非静态数据成员的值在任何时候都可以存储在一个联合中。
如果只存储一个值,如何读取另一个值?它只是不存在。
gcc 文档在实现定义的行为下列出了这一点
- 使用不同类型的成员 (C90 6.3.2.3) 访问联合对象的成员。
对象表示形式的相关字节被视为用于访问的类型的对象。请参阅类型关语。这可能是陷阱表示形式。
表示 C 标准不要求这样做。
2016-01-05:通过评论,我链接到 C99 缺陷报告 #283,其中添加了类似的文本作为 C 标准文档的脚注:
78a) 如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分被重新解释为新类型的对象表示,如6.2.6中所述(该过程有时称为“类型双关”)。这可能是陷阱表示形式。
不确定它是否澄清了很多,考虑到脚注不是标准的规范。
评论
我认为该标准最接近于说它是未定义的行为,它定义了包含公共初始序列的联合的行为(C99,§6.5.2.3/5):
为了简化联合的使用,做出了一项特殊保证:如果联合包含 共享一个共同初始序列的几个结构(见下文),如果并集 对象当前包含这些结构之一,允许检查公共结构 其中任何一个的初始部分,即联盟完整类型的声明是 可见。如果相应的成员具有 对于一个或多个序列的兼容类型(对于位域,具有相同的宽度) 初始成员。
C++11 在 §9.2/19 中给出了类似的要求/权限:
如果标准布局联合包含两个或多个共享公共初始序列的标准布局结构, 如果标准布局联合对象当前包含这些标准布局结构之一,则允许这样做 检查其中任何一个的共同初始部分。两个标准布局结构共享一个共同的首字母 如果相应成员具有布局兼容类型,并且两个成员都不是位字段或 对于一个或多个初始成员的序列,两者都是具有相同宽度的位域。
虽然两者都没有直接说明,但它们都带有强烈的暗示,即只有在 1) 成员是最近编写的成员(部分)或 2) 是共同初始序列的一部分时,“检查”(读取)成员才是“允许的”。
这并不是直接声明做其他事情是未定义的行为,但这是我所知道的最接近的行为。
评论
union
union
union
uint8_t
class Something { uint8_t myByte; [...] };
struct
现有答案中尚未提及的是第 6.2.5 节第 21 段中的脚注 37:
请注意,聚合类型不包括联合类型,因为对象 使用联合类型一次只能包含一个成员。
这个要求似乎清楚地暗示,你不能写一个成员,而读另一个成员。在这种情况下,由于缺乏规范,它可能是未定义的行为。
评论
混淆是 C 显式允许通过联合进行类型双关语,而 C++ (c++11) 没有这样的权限。
6.5.2.3 工会成员的结构
95) 如果用于读取联合对象内容的成员与上次用于读取的成员不同 在对象中存储一个值,该值的相应部分被重新解释为对象表示 作为 6.2.6 中描述的新类型中的对象表示(有时称为“类型”的过程 双关语'')。这可能是陷阱表示形式。
C++ 的情况:
9.5 联合 [class.union]
在联合中,任何时候最多可以有一个非静态数据成员处于活动状态,即 at 的值 大多数非静态数据成员可以随时存储在联合中。
C++ 后来的语言允许使用包含具有公共初始序列的 s 的并集;但是,这不允许类型双关语。struct
要确定 C++ 中是否允许联合类型双关语,我们必须进一步搜索。回想一下,c99 是 C++11 的规范性参考(C99 与 C11 具有类似的语言,允许联合类型双关):
3.9 类型 [basic.types]
4 - T 类型对象的对象表示形式是 N 个无符号字符对象的序列,由 类型为 T 的对象,其中 N 等于 sizeof(T)。对象的值表示是一组位,这些位 保存类型 T 的值。对于简单可复制的类型,值表示形式是对象中的一组位 确定值的表示形式,该值是实现定义的 值。42
42) 目的是 C++ 的内存模型与 ISO/IEC 9899 编程语言 C 的内存模型兼容。
当我们阅读时,它会变得特别有趣
3.8 对象生存期 [basic.life]
T 类型对象的生存期在以下情况下开始: — 获得具有 T 型适当对齐和大小的存储,并且 — 如果对象具有非平凡的初始化,则其初始化完成。
因此,对于包含在联合中的基元类型(事实上具有简单的初始化),对象的生存期至少包括联合本身的生存期。这允许我们调用
3.9.2 复合类型 [basic.compound]
如果 T 类型的对象位于地址 A 处,则 cv T* 类型的指针,其值为 地址 A 被称为指向该对象,而不管该值是如何获得的。
假设我们感兴趣的操作是类型双关语,即获取非活动联合成员的值,并且根据上述假设我们对该成员引用的对象有有效的引用,则该操作是左值到右值的转换:
4.1 左值到右值的转换 [conv.lval]
非函数、非数组类型的 glvalue 可以转换为 prvalue。 如果是不完整的类型,则需要此转换的程序格式不正确。如果 glvalue 引用的对象不是 T 类型的对象,也不是派生自
T
的类型的对象,或者如果该对象未初始化,则需要此转换的程序具有未定义的行为。
T
T
那么问题来了,作为非活动联合成员的对象是否通过存储初始化为活动联合成员。据我所知,情况并非如此,尽管如果:
- 将联合复制到阵列存储中并返回 (3.9:2),或者
char
- 将一个联合按字节复制到另一个相同类型的联合 (3.9:3),或者
- 一个并集由一个符合ISO/IEC 9899的程序元素跨语言边界访问(就其定义而言)(3.9:4注42),然后
非活动成员对联合的访问被定义,并且被定义为遵循对象和值表示,没有上述插入之一的访问是未定义的行为。这对允许在此类程序上执行的优化有影响,因为实现当然可以假设不会发生未定义的行为。
也就是说,尽管我们可以合法地为非活动联合成员形成左值(这就是为什么可以不构造地分配给非活动成员的原因),但它被认为是未初始化的。
评论
memcpy
unsigned char
*p
int *p = 0; const int *const *pp = &p;
int**
const int*const*
c
struct S s; const S &c = s;
&
&
我用一个例子很好地解释了这一点。
假设我们有以下联合:
union A{
int x;
short y[2];
};
我假设这给了 4,这给了 2。
当你写好它时,创建一个 A 类型的新 var,在其中输入值 10。sizeof(int)
sizeof(short)
union A 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(十进制)。
现在,如果你的并集有一个浮点数,或者双精度,你的记忆映射会更加混乱,因为你存储精确数字的方式。 更多信息你可以在这里得到。
评论