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

Luc*_*ore 114 c++ undefined-behavior unions language-lawyer

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

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

eca*_*mur 122

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

6.5.2.3结构和工会成员

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

C++的情况:

9.5工会[class.union]

在并集中,至多一个非静态数据成员可以在任何时间处于活动状态,也就是说,任何时候最多一个非静态数据成员的值都可以存储在并集中.

C++后来的语言允许使用包含struct具有共同初始序列的s 的联合; 然而,这并不允许打字.

为了确定C++中是否允许使用union type-punning ,我们必须进一步搜索.回想一下,是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大小的存储,并且 - 如果对象具有非平凡初始化,则其初始化完成.

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

3.9.2复合类型[basic.compound]

如果类型T的对象位于地址A,则类型为cv T*的指针(其值为地址A)被称为指向该对象,而不管该值是如何获得的.

假设我们感兴趣的操作是类型惩罚,即获取非活动联合成员的值,并且根据上面给出的我们对该成员引用的对象有有效引用,该操作是lvalue-to -rvalue转换:

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

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

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

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

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

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

  • 3.8/1表示对象的生命周期在其存储被重用时结束.这向我表明,工会生命周期中的非活动成员已经结束,因为其存储已被重用于活动成员.这意味着你在使用会员的方式上受到限制(3.8/6). (4认同)
  • 关于*“回想一下c99是C ++ 11的规范性引用”的一个问题**仅在c ++标准显式引用C标准的地方(例如,对于c库函数),这不仅仅相关吗? (3认同)
  • 在这种解释下,每一位内存同时包含所有类型的对象,这些对象可以通过简单的初始化并具有适当的对齐...因此,任何非平凡初始化类型的生命周期都会立即结束,因为它的存储空间将被重用于所有其他类型(而不是重新启动,因为它们不是简单的初始化)? (2认同)
  • 4.1的措词被完全彻底打破,此后已被重写。它不允许各种各样的完全有效的东西:它禁止自定义`memcpy`实现(使用`unsigned char` lvalues访问对象),它不允许在`int * p = 0之后访问`* p`;const int * const * pp =&p;`(即使从'int **`到`const int * const *`的隐式转换是有效的),它甚至不允许在`struct S s之后访问`c`。const S&c = s;`。[CWG第616期](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#616)。新的措辞允许吗?还有[basic.lval]。 (2认同)
  • @Omnifarious:这是有道理的,尽管它也需要澄清(并且C标准也需要澄清)一元'&`运算符应用于工会成员时意味着什么。我认为结果指针至少在下次下次直接或间接使用任何其他成员左值时才可用于访问该成员,但是在gcc中,指针不能使用那么长时间,这引发了一个问题: “&”运算符应该表示。 (2认同)

Bo *_*son 26

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

9.5工会

在并集中,至多一个非静态数据成员可以在任何时间处于活动状态,也就是说,任何时候最多一个非静态数据成员的值都可以存储在并集中.

如果只存储一个值,您如何阅读另一个值?它只是不存在.


gcc文档在Implementation defined行为下列出了这一点

  • 使用不同类型的成员(C90 6.3.2.3)访问union对象的成员.

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

表明C标准不要求这样做.


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

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

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

  • @LuchianGrigore:UB不是标准所说的UB,而是标准没有描述它应该如何工作.这正是这种情况.标准是否描述了会发生什么?它是否说它的实现定义了?不,不.所以这是UB.此外,关于"成员共享相同的内存地址"参数,您将不得不参考别名规则,这将再次带您到UB. (8认同)
  • @LuchianGrigore:根据定义部分,省略明确的行为定义也是未经考虑的未定义行为. (8认同)
  • @Luchian:很明显活动意味着什么,*"也就是说,最多一个非静态数据成员的价值可以随时存储在一个联合中."* (5认同)
  • @LuchianGrigore:是的.标准没有(也不能)解决的案例数量无限.(C++是一个图灵完整的虚拟机,所以它不完整.)那又怎样?它确实解释了"活跃"的意思,参考上面的引用,在"那是"之后. (5认同)
  • @Claudiu这是UB的原因不同 - 它违反了严格的别名. (5认同)
  • @ybungalobill实际上有很多标准表示行为未定义的地方.此外,还不清楚"活跃"是什么意思. (3认同)
  • 尝试读取不存在的对象(如使用悬空指针)是未定义的行为.联合只包含一个值,最后写入的值. (2认同)
  • @EitanT - 是的,未定义的行为是试图读取不存在的成员(并且这不是工会独有的:-).复杂性在于,如果你尝试这种类型的双关语,和其它编译器要成为GCC兼容GCC承诺尽力,所以他们允许这一点.所以它经常在实践中起作用,除非它没有,你遇到像*"这可能是陷阱表示"*这样的事情. (2认同)
  • @curiousguy 您不能通过不兼容的类型指针取消引用。在这种情况下,延迟为 `int*` 与 `char*` 不兼容。有 [一个例外](http://stackoverflow.com/questions/23848188/strict-aliasing-rule-and-char-pointers) 允许 `char*` 使用任何别名,但反过来不行。 (2认同)

Jer*_*fin 18

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

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

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

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

虽然两者都没有直接说明,但这两者都具有强烈的含义,即"检查"(阅读)成员只有在1)它是(最近一部分)成员(的一部分),或者2)是共同初始的一部分时才被"允许".序列.

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

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

mpu*_*mpu 12

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

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

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


归档时间:

查看次数:

15714 次

最近记录:

9 年,1 月 前