访问不活动的联盟成员和未定义的行为?

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

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

令人困惑的是,C明确允许通过一个联合进行types窜改,而C ++( c ++ 11 )却没有这样的权限。

C11

6.5.2.3结构和联盟成员

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

C ++的情况:

C ++ 11

9.5工会[class.union]

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

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

为了确定是否允许在C ++中使用联合types双击,我们必须进一步search。 回想一下, c99是C ++ 11的规范性参考(C99与C11具有类似的语言,允许使用联合types双关键字):

3.9types[basic.types]

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

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

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

typesT的对象的生命周期开始于以下情况: – 获得具有适当typesT的alignment和大小的存储,以及 – 如果对象具有不平凡的初始化,则其初始化完成。

因此,对于包含在一个联合中的一个原始types( 事实上它具有微不足道的初始化),该对象的生命周期至less包含联合本身的生命周期。 这使我们能够调用

3.9.2化合物types[basic.compound]

如果typesT的对象位于地址A处,则不pipe值如何获得,都将其值为地址A的types为cv T *的指针指向该对象。

假设我们感兴趣的操作是type-punning(即取非活动联合成员的值),并根据上述给出的那个对该成员引用的对象的有效引用,则该操作是左值价值转换:

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

一个非函数的非数组typesT的glvalue可以转换为一个prvalue。 如果T是不完整的types,那么需要这种转换的程序是不合格的。 如果glvalue引用的对象不是Ttypes的对象,也不是从T派生的types的对象,或者对象未初始化,则需要此转换的程序具有未定义的行为。

接下来的问题是一个非活动联合成员的对象是否被存储到活动联合成员中初始化。 据我所知,情况并非如此,虽然如果:

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

非活动成员对工会的访问被定义并被定义为遵循对象和价值表示,没有上述间接之一的访问是未定义的行为。 这对于允许在这样的程序上进行优化是有意义的,因为实现当然可以假定不发生未定义的行为。

也就是说,虽然我们可以合法地形成一个不活跃的联盟成员的左值(这就是为什么分配给没有build设的非活跃成员可以),但它被认为是未初始化的。

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

9.5工会

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

如果只有一个值被存储,你怎么读另一个值呢? 它只是不在那里。


gcc文档在Implementation定义的行为下列出了这个

  • 联合对象的成员可以使用不同types的成员来访问(C90 6.3.2.3)。

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

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


2016-01-05:通过评论,我被链接到C99缺陷报告#283 ,它在C标准文件中增加了一个类似的文字作为脚注:

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

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

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

为了简化联合的使用,我们做出了一个特殊的保证:如果一个联合包含多个共享一个共同初始序列的结构(见下文),并且联合对象当前包含这些结构中的一个,则允许检查共同的他们中任何一个的初始部分都可以看到整个工会types的声明。 如果对应的成员对于一个或多个初始成员的序列具有兼容的types(以及对于位域,相同的宽度),则两个结构共享一个共同的初始序列。

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

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

尽pipe两者都没有直接说明,但这两者都强烈暗示, 只有在1)它是最近成员的(部分)成员,或者2)是共同初始成员的一部分时,“检查​​”(阅读)序列。

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

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

请注意,聚合types不包含联合types,因为具有联合types的对象一次只能包含一个成员。

这个要求似乎显然意味着你不能写一个成员,而要读另一个成员。 在这种情况下,由于缺乏规范可能会导致未定义的行为。

我用一个例子来解释一下。
假设我们有以下联盟:

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

我很好的假设sizeof(int)给出了4,而sizeof(short)给出了2。
当你写union A a = {10} ,就可以创build一个新的Atypes的variables,把它放在10中。

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

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

正如你所看到的,ax的值是10,ay 1的值是10,而ay [0]的值是0。

现在,如果我这样做会发生什么?

 ay[0] = 37; 

我们的记忆将如下所示:

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

这将把ax的值变成2424842(十进制)。

现在,如果你的联盟有一个浮点数,或者两倍,你的记忆地图就会变得更加混乱,因为你存储确切的数字。 更多的信息,你可以在这里得到。