访问无效的工会成员和未定义的行为?


129

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

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


3
C99(我也相信C ++ 11也是如此)显式地允许使用联合类型操作。因此,我认为它属于“实现定义”行为。
Mysticial

1
我已经多次使用它来将单个int转换为char。因此,我绝对知道它不是不确定的。我在Sun CC编译器上使用了它。因此,它可能仍然取决于编译器。
go4sri 2012年

42
@ go4sri:显然,您不知道行为未定义意味着什么。它似乎在某些情况下对您有用的事实并不矛盾其不确定性。
本杰明·林德利

4
相关:C和C ++中的
联合的

4
@Mysticial,您链接到的博客文章特别针对C99;这个问题仅针对C ++标记。
davmac

Answers:


131

混乱之处在于C明确允许通过联合进行类型修饰,而C ++()没有此类许可。

6.5.2.3结构和工会成员

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

C ++的情况:

9.5工会[class.union]

在联合中,最多可以在任何时间激活一个非静态数据成员,即,可以随时将一个最多非静态数据成员的值存储在一个联合中。

后来C ++的语言允许使用struct带有s且具有共同初始序列的并集。但是,这不允许进行类型调整。

为了确定在C ++中是否允许联合类型修剪,我们必须进一步搜索。回想起那个 是C ++ 11的规范性参考(并且C99与C11的语言类似,允许联合类型对齐):

3.9类型[basic.types]

4-类型T的对象的对象表示形式是由类型T的对象占用的N个无符号字符对象的序列,其中N等于sizeof(T)。对象的值表示形式是持有类型T值的位集合。对于普通可复制类型,值表示形式是对象表示形式中确定值的位集合,该值是实现的一个离散元素-定义的一组值。42
42)目的是C ++的存储模型与ISO / IEC 9899编程语言C的存储模型兼容。

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

3.8对象生存期[basic.life]

T类型的对象的生命周期在以下时间开始:-获得类型T具有正确的对齐方式和大小的存储,并且-如果该对象具有非平凡的初始化,则其初始化完成。

因此,对于包含在并集中的基本类型(即,ipso实际上具有微不足道的初始化),对象的生存期至少包含并集本身的生存期。这使我们可以调用

3.9.2化合物类型[basic.compound]

如果类型T的对象位于地址A,则将其值为地址A的cv T *类型的指针指向该对象,而不管如何获取该值。

假设我们感兴趣的操作是类型操作,即获取一个非活动联合成员的值,并且根据以上所述,我们对该成员所引用的对象具有有效的引用,那么该操作就是左值-右值转换:

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

非功能,非数组类型的glvalue T可以转换为prvalue。如果T是不完整的类型,则必须进行此转换的程序格式错误。如果glvalue所引用的对象不是类型的对象,T也不是从派生的类型T的对象,或者该对象未初始化,则需要进行此转换的程序将具有未定义的行为。

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

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

如果定义了非活动成员对并集的访问,并且该访问被定义为遵循对象和值表示形式,则没有上述中介之一的访问是未定义的行为。这对允许对此类程序执行的优化有影响,因为该实现当然可以假定未发生未定义的行为。

也就是说,尽管我们可以合法地为非活动的工会成员形成一个左值(这就是为什么无需构造即可分配给非活动的成员是可以的),但它仍被认为是未初始化的。


5
3.8 / 1表示对象的生命周期在重用其存储时结束。这向我表明,工会生命周期中的非活动成员已经结束,因为其存储已重新用于活动成员。这意味着您在使用该成员时会受到限制(3.8 / 6)。
bames53

2
在这种解释下,内存的每个位同时包含所有类型的对象,这些对象都是可以初始化的并且具有适当的对齐方式...那么,任何不可初始化的类型的生存期都会立即结束,因为其存储将被所有其他类型重用(而不是重新启动,因为它们无法轻松初始化)?
bames53

3
4.1的措词被完全彻底打破,此后已被重写。它不允许各种完全有效的东西:它禁止自定义memcpy实现(使用unsigned char左值访问对象),不允许对*pafter的访问int *p = 0; const int *const *pp = &p;(即使从int**to 的隐式转换const int*const*是有效的),甚至不允许对cafter的访问struct S s; const S &c = s;CWG问题616。新的措辞允许吗?还有[basic.lval]。

2
@Omnifarious:这是有道理的,尽管它也需要弄清楚一元运算&符应用于工会成员时的含义(以及C标准也需要弄清楚)。我认为结果指针至少在下一次直接使用或间接使用任何其他成员左值之前都应该可用于访问该成员,但是在gcc中,指针不能使用那么长时间,这就提出了一个问题:该&运营商的解释是:。
超级猫

4
关于“回想c99是C ++ 11的规范性引用”的一个问题不是唯一相关的,因为c ++标准明确地引用了C标准(例如,对于c库函数)?
MikeMB

28

C ++ 11标准这样说

9.5联盟

在联合中,最多可以在任何时间激活一个非静态数据成员,即,可以随时将一个最多非静态数据成员的值存储在一个联合中。

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


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

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

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

表示这不是C标准所必需的。


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

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

考虑到脚注不是该标准的规范,因此不确定是否可以澄清。


10
@LuchianGrigore:UB不是标准所说的UB,而是标准没有描述它应该如何工作的东西。正是这种情况。该标准是否描述了会发生什么?它是否表示已定义实现?不,不。所以是UB。此外,关于“成员共享相同的内存地址”参数,您必须引用别名规则,这将使您再次进入UB。
Yakov Galka

5
@Luchian:很明显active是什么意思,即“随时可以将最多一个非静态数据成员的值存储在一个联合中”。
本杰明·林德利

5
@LuchianGrigore:是的。该标准没有(也不能)解决的案例数不胜数。(C ++是图灵完整的VM,因此它是不完整的。)那又如何呢?它确实解释了“活动”的含义,请在“即”之后引用以上引用。
Yakov Galka'7年

8
@LuchianGrigore:根据定义部分,未明确定义行为也是未考虑的未定义行为。
jxh 2012年

5
@Claudiu这是UB的另一个原因-它违反了严格的别名。
Mysticial

18

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

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

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

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

尽管都没有直接声明,但它们都带有强烈的含义,即只有在1)它是最近写入的成员的一部分或2)是公共首字母的一部分时, “检查”(读取)成员是“允许的”。序列。

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


为了使这个完整的,你需要知道什么是“布局兼容的类型”是C ++,或者“兼容类型”是C.
迈克尔·安德森

2
@MichaelAnderson:是和否。您需要在何时/是否要确定某些东西是否属于此异常的情况下进行处理-但真正的问题是,明显超出该异常的东西是否真的给了UB。我认为在此暗示的意图足够明确,但我认为从来没有直接说明过。
杰里·科芬

这种“常见的初始顺序”可能只是从重写箱中保存了我的2或3个项目。当我第一次阅读有关unions的大多数拙劣用法时,我感到非常高兴,因为某个博客给我留下了这样的印象,那就是可以,并围绕它构建了多个大型结构和项目。现在我我可能还是可以的,因为我的unions确实包含了在前面具有相同类型的类
underscore_d

@JerryCoffin,我想您暗示的是与我相同的问题:如果我们union包含例如 a uint8_t和a- class Something { uint8_t myByte; [...] };我会认为此附加条件也将在这里适用,但措辞非常刻意仅允许structs。幸运的是,我已经在使用那些原始而非原始的原始内容了:O
underscore_d

@underscore_d:C标准至少涵盖了以下问题:“指向经过适当转换的结构对象的指针指向其初始成员(或者,如果该成员是位字段,则指向它所驻留的单元) ,反之亦然。”
杰里·科芬,2015年

12

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

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

这项要求似乎清楚地意味着您不能写成员,也不能读另一个成员。在这种情况下,由于缺乏规范,这可能是不确定的行为。


许多实现记录了它们的存储格式和布局规则。在没有规则说编译器不必实际使用其定义的存储格式的情况下(除非使用指针读取和写入内容时),在许多情况下,这样的规范将暗示一种类型的存储的读取和另一种类型的存储所产生的影响。字符类型。
超级猫

-3

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

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

如您所见,ax的值为10,ay 1的值为10,ay [0]的值为0。

现在,如果我这样做会怎么样?

a.y[0] = 37;

我们的记忆将如下所示:

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

这会将ax的值转换为2424842(十进制)。

现在,如果您的并集具有浮点数或两倍数,则由于存储精确数字的方式,您的内存映射将更加混乱。更多信息,你可以在这里


18
:)这不是我问的。我知道内部会发生什么。我知道这行得通。我问它是否在标准中。
Luchian Grigore 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.