访问不活跃的工会成员和未定义的行为?

Accessing inactive union member and undefined behavior?

提问人:Luchian Grigore 提问时间:7/7/2012 最后编辑:Aykhan HagverdiliLuchian Grigore 更新时间:3/25/2020 访问量:28458

问:

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

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

C++ 未定义行为 语言-律师 联合 C11 C99

评论

3赞 Mysticial 7/7/2012
C99(我相信 C++11 也是如此)明确允许使用联合进行类型双关语。所以我认为它属于“实现定义”的行为。
1赞 go4sri 7/7/2012
我曾多次使用它从单个 int 转换为 char。所以,我绝对知道它不是未定义的。我在 Sun CC 编译器上使用了它。因此,它可能仍然依赖于编译器。
52赞 Benjamin Lindley 7/7/2012
@go4sri:显然,你不知道行为不定义意味着什么。在某些情况下,它似乎对你有用,这一事实与它的未定义性并不矛盾。
4赞 legends2k 10/11/2013
相关:C 和 C++ 中联合的目的
5赞 davmac 12/3/2013
@Mysticial,您链接到的博客文章非常具体地介绍了 C99;此问题仅针对 C++ 进行标记。

答:

31赞 Bo Persson 7/7/2012 #1

C++11标准是这样说的

9.5 工会

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

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


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

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

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

表示 C 标准不要求这样做。


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

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

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

评论

13赞 Yakov Galka 7/7/2012
@LuchianGrigore:UB不是标准所说的UB,而是标准没有描述它应该如何工作的东西。这正是这种情况。该标准是否描述了会发生什么?它是否说它是定义的实现?不,不。所以它是UB。此外,关于“成员共享相同的内存地址”参数,您必须参考别名规则,这将再次将您带到 UB。
5赞 Benjamin Lindley 7/7/2012
@Luchian:Active 的含义很清楚,“也就是说,最多一个非静态数据成员的值可以随时存储在联合中。
6赞 Yakov Galka 7/7/2012
@LuchianGrigore:是的。该标准没有(也不能)解决的情况是无限多的。(C++ 是图灵完备的 VM,因此它是不完整的。那又怎样?它确实解释了“活动”的含义,请参阅上面的引文,在“即”之后。
9赞 jxh 7/7/2012
@LuchianGrigore:根据定义部分,省略行为的明确定义也是未考虑的未定义行为。
5赞 Mysticial 7/8/2012
@Claudiu 这是 UB 的另一个原因 - 它违反了严格的锯齿。
19赞 Jerry Coffin 8/11/2012 #2

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

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

C++11 在 §9.2/19 中给出了类似的要求/权限:

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

虽然两者都没有直接说明,但它们都带有强烈的暗示,即只有在 1) 成员是最近编写的成员(部分)或 2) 是共同初始序列的一部分时,“检查”(读取)成员才是“允许的”。

这并不是直接声明做其他事情是未定义的行为,但这是我所知道的最接近的行为。

评论

0赞 Michael Anderson 8/15/2012
为了完成这一点,你需要知道 C++ 的“布局兼容类型”是什么,或者 C 的“兼容类型”是什么。
2赞 Jerry Coffin 8/15/2012
@MichaelAnderson:是的,也不是。当你/如果你想确定某些东西是否属于这个例外时,你需要处理这些问题——但这里真正的问题是,明显属于这个例外的东西是否真的给了 UB。我认为这足以明确表达意图,但我认为它从未直接说明过。
0赞 underscore_d 12/31/2015
这个“通用初始序列”的东西可能只是从重写箱中保存了我的 2 或 3 个项目。当我第一次读到大多数双关语用法 s 是未定义的时,我很生气,因为一个特定的博客给我的印象是这没问题,并围绕它构建了几个大型结构和项目。现在我想我可能毕竟没问题,因为我的 s 确实包含前面具有相同类型的类unionunion
0赞 underscore_d 12/31/2015
@JerryCoffin,我想你在暗示和我一样的问题:如果我们的包含例如 a 和 a 怎么办 - 我认为这个但书也适用于这里,但它的措辞非常刻意地只允许 s。 幸运的是,我已经在使用这些而不是原始原语:Ounionuint8_tclass Something { uint8_t myByte; [...] };struct
0赞 Jerry Coffin 1/1/2016
@underscore_d:C 标准至少在某种程度上涵盖了这个问题:“指向结构对象的指针,经过适当转换,指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然。
12赞 mpu 8/17/2012 #3

现有答案中尚未提及的是第 6.2.5 节第 21 段中的脚注 37:

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

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

评论

0赞 supercat 9/21/2016
许多实现都记录了其存储格式和布局规则。在许多情况下,这样的规范意味着在没有规则的情况下,读取一种类型的存储并作为另一种类型写入的效果,除非使用字符类型的指针读取和写入内容。
161赞 ecatmur 8/17/2012 #4

混淆是 C 显式允许通过联合进行类型双关语,而 C++ () 没有这样的权限。

6.5.2.3 工会成员的结构

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

C++ 的情况:

9.5 联合 [class.union]

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

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

要确定 C++ 中是否允许联合类型双关语,我们必须进一步搜索。回想一下, 是 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 的类型的对象,或者如果该对象未初始化,则需要此转换的程序具有未定义的行为。TT

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

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

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

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

评论

6赞 bames53 10/19/2012
3.8/1 表示当对象的存储被重用时,对象的生命周期结束。这向我表明,工会生命周期中的非活跃成员已经结束,因为它的存储已被活跃成员重新使用。这意味着您在使用成员的方式上受到限制 (3.8/6)。
2赞 bames53 10/19/2012
在这种解释下,内存的每一位都同时包含所有类型的对象,这些对象是可简单初始化的,并且具有适当的对齐方式......那么,任何非平凡的可初始化类型的生存期是否会立即结束,因为它的存储被重用于所有这些其他类型(并且不会重新启动,因为它们不是平凡的可初始化的)?
3赞 9/14/2014
措辞 4.1 完全被破坏,此后被重写。它不允许各种完全有效的东西:它不允许自定义实现(使用 lvalues 访问对象),它不允许访问 after(即使从 to 的隐式转换是有效的),它甚至不允许访问 after 。CWG 第 616 期。新措辞是否允许?还有 [basic.lval]。memcpyunsigned char*pint *p = 0; const int *const *pp = &p;int**const int*const*cstruct S s; const S &c = s;
2赞 supercat 4/24/2017
@Omnifarious:这是有道理的,尽管它还需要澄清(顺便说一句,C 标准也需要澄清)一元运算符在应用于工会成员时的含义。我认为生成的指针应该至少在下次直接或间接使用任何其他成员 lvalue 之前可用于访问该成员,但在 gcc 中,指针甚至不能使用那么长,这就提出了一个问题运算符应该是什么意思。&&
4赞 MikeMB 9/15/2017
关于“回想一下 c99 是 C++11 的规范参考”的一个问题,这难道不是唯一相关的,其中 c++ 标准明确引用 C 标准(例如,对于 c 库函数)?
-5赞 elyashiv 8/17/2012 #5

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

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(十进制)。

现在,如果你的并集有一个浮点数,或者双精度,你的记忆映射会更加混乱,因为你存储精确数字的方式。 更多信息你可以在这里得到。

评论

21赞 Luchian Grigore 8/17/2012
:)这不是我问的。我知道内部发生了什么。我知道它有效。我问它是否在标准中。