访问非活动的union成员和未定义的行为?

时间:2012-07-07 07:37:00

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

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

那么,这是不确定的行为吗?

5 个答案:

答案 0 :(得分:114)

混淆是C明确允许通过联合进行类型惩罚,而C ++()没有这样的权限。

  

     

6.5.2.3结构和工会成员

     

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

C ++的情况:

  

     

9.5联盟[class.union]

     

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

C ++后来的语言允许使用包含struct个共同初始序列的联合;但这并不允许打字。

要确定C ++中是否允许使用union type-punning ,我们必须进一步搜索。回想一下,是C ++ 11的规范性引用(C99与C11具有类似的语言,允许联合类型 - 惩罚):

  

3.9类型[basic.types]

     

4 - 类型T对象的对象表示是由N占用的N个无符号字符对象的序列   类型为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 Lvalue-to-rvalue转换[conv.lval]

     

非函数非数组类型T的glvalue可以转换为prvalue。   如果T是不完整类型,则需要进行此转换的程序格式不正确。 如果glvalue引用的对象不是T类型的对象,并且不是从T派生的类型的对象,或者如果对象未初始化,则需要这样的程序转换具有未定义的行为。

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

  • 将一个union复制到char数组存储中并返回(3.9:2)或
  • 将union以字节方式复制到另一个相同类型的联合(3.9:3)或
  • 通过符合ISO / IEC 9899的程序元素跨越语言边界访问联合(到目前为止)(3.9:4注释42),然后

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

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

答案 1 :(得分:24)

C ++ 11标准就是这样说的

  

9.5联盟

     

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

如果只存储一个值,您如何阅读另一个值?它就在那里。


gcc文档在Implementation defined behavior

下列出了这一点
  
      
  • 使用不同类型的成员(C90 6.3.2.3)访问union对象的成员。
  •   
     

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

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


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

  

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

虽然脚注不是标准的规范,但不确定它是否澄清了很多。

答案 2 :(得分:16)

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

  

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

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

  

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

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

这不是一个直接声明,否则是未定义的行为,但它是我所知道的最接近的行为。

答案 3 :(得分:10)

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

  

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

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

答案 4 :(得分:-2)

我用一个例子来解释这一点 假设我们有以下联合:

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.y 1的值为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(十进制)。

现在,如果你的联盟有一个浮点数或两倍,那么你的记忆地图就会变得更加混乱,因为存储确切数字的方式。 您可以在here获得更多信息。