【问题标题】:Accessing inactive union member and undefined behavior?访问不活动的工会成员和未定义的行为?
【发布时间】:2012-07-07 13:29:03
【问题描述】:

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

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

【问题讨论】:

  • C99(我相信 C++11 也是如此)明确允许使用联合进行类型双关。所以我认为它属于“实现定义”的行为。
  • 我曾多次使用它来将单个 int 转换为 char。所以,我绝对知道它不是未定义的。我在 Sun CC 编译器上使用了它。所以,它可能仍然依赖于编译器。
  • @go4sri:显然,你不知道行为未定义意味着什么。在某些情况下它似乎对你有用的事实与它的不确定性并不矛盾。
  • @Mysticial,您链接到的博客文章非常专门针对 C99;此问题仅针对 C++ 标记。

标签: c++ undefined-behavior language-lawyer unions


【解决方案1】:

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

6.5.2.3 结构和联合成员

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

C++的情况:

9.5 联合 [class.union]

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

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

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

3.9 类型 [basic.types]

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

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

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

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

因此,对于包含在联合中的原始类型(ipso facto 具有简单的初始化),对象的生命周期至少包含联合本身的生命周期。这允许我们调用

3.9.2 复合类型 [basic.compound]

如果 T 类型的对象位于地址 A,则为 cv T* 类型的指针,其值为 无论值是如何获得的,地址 A 都被称为指向该对象。

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

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

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

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

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

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

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

【讨论】:

  • 3.8/1 表示对象的生命周期在其存储被重用时结束。这向我表明,工会生命周期中的非活动成员已经结束,因为它的存储已被活动成员重用。这意味着您使用会员的方式受到限制 (3.8/6)。
  • 在这种解释下,内存的每一位同时包含所有类型的对象,这些对象可以平凡初始化并具有适当的对齐方式......那么任何非平凡初始化类型的生命周期都会立即作为其存储结束被所有其他类型重用(而不是重新启动,因为它们不是很容易初始化)?
  • 措辞 4.1 完全彻底地被打破,并且已经被重写。它不允许各种完全有效的事情:它不允许自定义memcpy 实现(使用unsigned char 左值访问对象),它不允许在int *p = 0; const int *const *pp = &p; 之后访问*p(即使从int** 到@987654337 的隐式转换@ 有效),即使在struct S s; const S &c = s; 之后访问c 也是不允许的。 CWG issue 616。新措辞允许吗?还有 [basic.lval]。
  • @Omnifarious:这是有道理的,尽管它还需要澄清(顺便说一句,C 标准也需要澄清)一元 & 运算符在应用于联合成员时的含义。我认为生成的指针应该至少可以用于访问该成员,直到下一次直接或间接使用任何其他成员左值,但在 gcc 中,指针即使那么长也不可用,这就提出了一个问题:什么& 运算符应该是指。
  • 关于“回想一下,c99 是 C++11 的规范性参考”的一个问题这不仅仅是相关的,c++ 标准明确引用了 C 标准(例如对于 c 库函数)?
【解决方案2】:

C++11 标准是这样说的

9.5 联合

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

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


gcc 文档在Implementation defined behavior 下列出了这个

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

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

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


2016-01-05:通过 cmets,我链接到 C99 Defect Report #283,它在 C 标准文档中添加了类似的文本作为脚注:

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

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

【讨论】:

  • @LuchianGrigore:UB 不是标准所说的 UB,而是标准没有描述它应该如何工作的东西。这正是这种情况。标准是否描述了会发生什么?它是否说它的实现已定义?没有也没有。所以是UB。此外,关于“成员共享相同内存地址”的说法,您必须参考别名规则,这将带您再次进入 UB。
  • @Luchian: 很清楚active是什么意思,“就是在任何时候都可以将最多一个非静态数据成员的值存储在一个union中。”
  • @LuchianGrigore:是的。标准没有(也不能)解决的情况有无数种。 (C++ 是一个图灵完备的虚拟机,所以它是不完整的。)那又怎样?它确实解释了“活跃”的含义,请参阅上面的引用,在“那是”之后。
  • @LuchianGrigore:根据定义部分,省略明确的行为定义也是未考虑的未定义行为。
  • @Claudiu 那是 UB,原因不同——它违反了严格的别名。
【解决方案3】:

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

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

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

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

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

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

【讨论】:

  • 要完整,您需要知道 C++ 的“布局兼容类型”或 C 的“兼容类型”。
  • @MichaelAnderson:是的,也不是。当/如果您想确定某些东西是否属于此例外时,您需要处理这些问题——但这里真正的问题是,明显不属于例外的东西是否真的给了 UB。我认为这已经足够强烈地暗示了意图,但我认为它从未被直接陈述过。
  • 这个“通用初始序列”可能只是从 Rewrite Bin 中保存了我的 2 或 3 个项目。当我第一次读到unions 的大多数双关语用法未定义时,我很生气,因为某个博客给我的印象是这没问题,并围绕它构建了几个大型结构和项目。现在我认为毕竟我可能没问题,因为我的unions 在前面确实包含具有相同类型的类
  • @JerryCoffin,我想你在暗示和我一样的问题:如果我们的 union 包含 eg 一个 uint8_t 和一个 class Something { uint8_t myByte; [...] }; - 我会假设这个附带条件也适用于此,但它的措辞非常刻意,只允许structs。幸运的是,我已经在使用这些而不是原始原语:O
  • @underscore_d:C 标准至少在某种程度上涵盖了这个问题:“一个指向结构对象的指针,经过适当转换,指向它的初始成员(或者如果该成员是位字段,那么到它所在的单元),反之亦然。”
【解决方案4】:

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

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

此要求似乎明确暗示您不得在一个成员中写入并在另一个成员中读取。在这种情况下,由于缺乏规范,它可能是未定义的行为。

【讨论】:

  • 许多实现都记录了它们的存储格式和布局规则。在许多情况下,这样的规范将暗示在没有规则说编译器不必实际使用其定义的存储格式除非使用指针读取和写入内容时,读取一种类型的存储并像另一种类型一样写入的效果是什么字符类型。
【解决方案5】:

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

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| -----------------------------------------

如你所见,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(十进制)。

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

【讨论】:

  • :) 这不是我问的。我知道内部会发生什么。我知道它有效。我问它是否在标准中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-12
  • 1970-01-01
  • 1970-01-01
  • 2021-11-15
  • 1970-01-01
  • 2014-11-22
相关资源
最近更新 更多