是否通过 C99 中未指定的联合进行类型双关,并且它是否已在 C11 中指定?

Posted

技术标签:

【中文标题】是否通过 C99 中未指定的联合进行类型双关,并且它是否已在 C11 中指定?【英文标题】:Is type-punning through a union unspecified in C99, and has it become specified in C11? 【发布时间】:2012-07-23 07:22:18 【问题描述】:

Stack Overflow 问题Getting the IEEE Single-precision bits for a float 的一些答案建议使用union 结构进行类型双关语(例如:将float 的位转换为uint32_t):

union 
    float f;
    uint32_t u;
 un;
un.f = your_float;
uint32_t target = un.u;

但是,根据 C99 标准(至少是草案 n1124),工会的 uint32_t 成员的值似乎未指定,其中第 6.2.6.1.7 节规定:

当一个值存储在联合类型对象的成员中时,对象表示中与该成员不对应但与其他成员对应的字节采用未指定的值。

C11 n1570 草案的至少一个脚注似乎暗示这不再是这种情况(参见 6.5.2.3 中的脚注 95):

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

但是,C99 草案中的第 6.2.6.1.7 节的文本与 C11 草案中的相同。

这种行为在 C99 下实际上是未指定的吗?是否已在 C11 中指定?我意识到大多数编译器似乎都支持这一点,但如果知道它是在标准中指定的,还是只是一个非常常见的扩展,那就太好了。

【问题讨论】:

技术说明:访问存储的最后一个以外的联合成员不会导致程序违反 C 标准。访问这样的联合成员会导致未指定的值(不是未定义的行为),并且根据 C 1999 4 3,“应是正确的程序并按照 5.1.2.3 行事。”此外,编译器可能会提供有关该值的额外保证并保持一致的实现。 @DanielFischer:n1124 和 n1570 草案都明确列出未指定:附录 J 中的“除了最后一个存储到 (6.2.6.1) 中的联合成员的值”(可移植性问题)。对我来说,这似乎意味着可能存在一个 C99(或 C11)编译器,其中使用联合进行类型双关语不能达到我们的预期。 再读一遍,它说那些 bytes 对应于另一个成员,而不是那些被写入具有未指定值的成员。这意味着与该成员相对应的字节(因此两者共有的字节)具有特定值,即写入的那个值。这一段只是用来解释未写入的字节会发生什么(或不发生),仅此而已。 @sfstewman,附录 J 不规范。 @EricPostpischil:如果在写入第一个值和读取第二个值之间,代码要检查联合字段占用的字节,标准将指示这些字节必须包含的内容。我不知道旧标准中的任何内容都会阻止编译器,例如将联合中的 float 优化为 FPU 寄存器,并将其覆盖的 int 优化为 CPU 寄存器,并仅在 char* 别名规则强制执行时将这些寄存器读/写到内存。 【参考方案1】:

使用 union 的类型双关语的行为从 C89 更改为 C99。 C99 中的行为与 C11 相同。

正如Wug 在他的回答中指出的那样,C99 / C11 中允许使用双关语。当联合成员的大小不同时,会读取可能是陷阱的未指定值。

脚注是 C99 在 Clive D.W. 之后添加的。羽Defect Report #257:

最后,从 C90 到 C99 的变化之一是取消了当最后一个商店指向不同的商店时访问联合的一个成员的任何限制。 基本原理是,该行为将随后取决于值的表示。由于这一点经常被误解,因此可能值得在标准中明确说明。

[...]

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

克莱夫 D.W. 的措辞Feather 在 C 委员会对Defect Report #283 的回答中被接受为技术勘误。

【讨论】:

DR不清楚,脚注不规范,只能解释其他地方的定义。此外,DR真的没有澄清任何事情。这个问题很混乱,因为 WG 很困惑。 (另外,Wug 在“类型双关语”的含义上是错误的。) 引用的文字似乎无法支持您的结论。 这可能是一个陷阱表示。它说。 @andrewrk 我的结论是类型双关语在 C99 和 C11 中是允许的。在写入成员后您可以读取另一个成员的陷阱表示这一事实不会改变这个结论。这意味着在某些具有某些特定值的系统上,您可以调用未定义的行为。类似地,如果您将 * 二元运算符与一些特定的操作数值一起使用,您也容易出现未定义的行为(有符号整数溢出),这并不意味着该运算符是 UB per se 或不允许用过。 @curiousguy 我的观点是,他们选择通过脚注并不是消除混乱的最佳方式。他们还应该修改 6.2.6.1p7(在 C99 中)以使事情更加清晰和规范。 即使它们是 same 大小,您也可以获得陷阱表示。不知道你为什么要为不同尺寸的箱子挑选出来。【参考方案2】:

原始的 C99 规范未指定此内容。

对 C99(我认为是 TR2)的一项技术勘误添加了脚注 82 以纠正这一疏忽:

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

该脚注保留在 C11 标准中(它是 C11 中的脚注 95)。

【讨论】:

我认为我们已经在上面讨论了问题,在 cmets 中,这是由 1999 C 标准指定的,对于相同大小的成员,至少需要一个符合要求的实现定义足以推断价值的信息。 @EricPostpischil "需要一致的实现来定义足以推断价值的信息" 哪里需要? @curiousguy:正如该问题的 cmets 中所述,C 1999 6.2.6.1 2(以及 C 2011 中的同一段落)说明了构成对象的字节的数量、顺序和编码是明确指定(由标准)或由实现定义。 @EricPostpischil 谢谢。 (我觉得这个事实有点令人惊讶。) 不幸的是,写这篇文章的人没有考虑到,当一个函数被传递给一个联合对象的不同成员的指针时,在写入一个联合成员并读取另一个联合成员时缺少定义的值是证明编译器行为的必要条件.如果在没有首先转换为“char”或使用 memcpy 的情况下获取联合成员的地址并使用结果指针是合法的,那么标准中的任何内容都无法证明通过指针写入一个联合成员并读取另一个联合成员通常具有与写入不同的行为这一事实并直接阅读工会成员。【参考方案3】:

这一直是“不确定的”。正如其他人所指出的,通过技术更正向 C99 添加了脚注。内容如下:

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

但是,前言中的脚注是非规范性的:

附件 D 和 F 构成本标准的规范性部分;附件 A、B、C、E、G、H、I、J、参​​考书目和索引仅供参考。根据 ISO/IEC 指令的第 3 部分,本前言、引言、注释、脚注和示例也仅供参考

也就是说,脚注不能禁止行为;他们只应澄清现有案文。这是一个不受欢迎的观点,但上面引用的脚注在这方面实际上是失败的——规范文本中没有禁止这种行为。确实有矛盾的部分,比如6.7.2.1:

...任何时候最多可以将其中一个成员的值存储在联合对象中

结合 6.5.2.3(关于使用“.”操作符访问联合成员):

值是命名成员的值

即如果只能存储一个成员的值,则另一个成员的值不存在。这强烈暗示通过联合的类型双关应该是可能的;成员访问产生一个不存在的值。 C11 文档中仍然存在相同的文本。

但是,很明显,添加脚注的目的是允许类型双关;只是委员会似乎违反了不包含规范性文本的脚注规则。要接受脚注,您真的必须忽略脚注不是规范性的部分,或者试图弄清楚如何以支持脚注结论的方式解释规范性文本(我已经尝试过,并且失败,做)。

关于批准脚注,我们能做的最好的事情是对从 6.2.5 起将联合定义为一组“重叠对象”做出一些假设:

联合类型描述了一组重叠的非空成员对象,每个对象都有一个可选的指定名称和可能不同的类型

不幸的是,没有详细说明“重叠”的含义。对象被定义为(3.14)“执行环境中的数据存储区域,其内容可以表示值”(即相同的存储区域可以由两个或多个distinct 对象由上面的“重叠对象”定义暗示,即对象具有与其存储区域分开的标识)。合理的假设似乎是(特定联合实例的)联合成员使用相同的存储区域。

即使我们忽略 6.7.2.1/6.5.2.3 并允许(如脚注所示)读取任何联合成员返回将由相应存储区域的内容表示的值 - 因此将允许类型双关——6.5 中一直存在问题的严格别名规则不允许(除了某些小例外)访问对象,而不是按其类型。由于“访问”是(3.1)“读取或修改对象的值”,并且由于修改一组重叠对象中的一个必然会修改其他对象,因此严格混叠规则可以写入工会成员可能会违反(无论是否通过另一个成员读取)。

例如,按照标准的措辞,以下内容是非法的:

union 
   int a;
   float b;
 u;

u.a = 0; // modifies a float object by an lvalue of type int
int *pa = &u.a;
*pa = 1; // also modifies a float object, without union lvalue involved

(具体来说,注释的两行违反了严格的别名规则)。

严格来说,脚注涉及一个单独的问题,即阅读不活跃的工会成员;然而,严格别名规则与上面提到的其他部分一起严重限制了它的适用性,特别是意味着它通常不允许类型双关语(但仅适用于特定的类型组合)。

令人沮丧的是,负责制定标准的委员会似乎打算通过联合来实现类型双关语,但似乎并不担心标准文本仍然不允许它。

还值得注意的是,(编译器供应商)的共识理解似乎是允许通过联合进行类型双关,但“必须通过联合类型访问”(例如上面示例中的第一个注释行,但是不是第二个)。有点不清楚这是否应该同时适用于读取和写入访问,并且标准文本绝不支持(忽略脚注)。

总结:虽然通过联合的类型双关语在很大程度上被接受是合法的(大多数人认为只有在“通过联合类型”完成访问时才允许,可以这么说),标准的措辞禁止它在除了某些琐碎的案例之外的所有情况。

你引用的部分:

当一个值存储在联合类型对象的成员中时,对象表示中与该成员不对应但与其他成员对应的字节采用未指定的值。

...不过,必须仔细阅读。 “对象表示的字节不对应于该成员”是指超出成员大小的字节,这本身不是类型双关的问题(除非你不能假设写作对工会成员来说,任何较大成员的“额外”部分都不会受到影响)。

【讨论】:

嗯...§ 6.2.5/20 声明工会成员重叠。将此与“任何时候最多可以将一个成员的值存储在联合对象中”结合起来,可以将后者解释为说明这个重叠的存储空间一次只能包含一个成员的值,这通过扩展意味着所有非活动成员在那时将是部分或全部活动成员的替代视图(由于寻址相同的重叠存储空间)。 例如,给定typedef union int i; char c; U;U u;,如果u.i 被赋值为5,那么u.c 将是整数文字5 的字节之一,因为共享同一个存储空间。因此,工会成员重叠的这一任务是允许解释支持类型双关语的关键。 感觉这不应该是一个有效的解释,但它在技术上确实符合标准的文字。 @JustinTime “这通过扩展意味着所有非活动成员当时将是部分或全部活动成员的替代视图(由于解决相同的重叠存储空间)” - 即 IMO ,外推但不是明确的逻辑必要性;问题在于,除了通过非规范脚注之外,并没有真正指定“重叠”所需要的内容,并且对象的概念仅仅是一种关于存储的观点与例如。严格的别名规则。 @JustinTime:在 C89 下,给定 union T1 v1; T2 v2; u;u.v1 = thing1; thing2 = u.v2;T1 *p1=&u.v1; T2 *p2=&u.v2; *p1=thing1; thing2=*p2; 的行为将由标准在相同的情况下定义(例如那些涉及兼容类型或通用初始序列的情况)规则),并且在所有其他中都是实现定义的。根据所使用的左值的形式,实现可能可以对此类访问进行不同的处理,但 C89 中没有任何内容做出此类区分。 C89 不能在第一种情况下允许类型双关语,而在第二种情况下不做同样的事情。【参考方案4】:

但是,这似乎违反了 C99 标准(至少是 n1124 草案),其中第 6.2.6.1.7 节规定了一些内容。这种行为在 C99 下实际上是未指定的吗?

不,你很好。

当一个值存储在联合类型对象的成员中时,对象表示中与该成员不对应但与其他成员对应的字节采用未指定的值。

这适用于不同大小的数据块。即,如果您有:

union u

    float f;
    double d;
;

如果你给 f 赋值,它会改变 d 的低 4 字节,但高 4 字节会处于不确定状态。

联合主要用于类型双关语。

【讨论】:

联合不存在仅用于类型双关语。存在联合是因为有时您想存储一种类型的对象,然后再检索它,有时您想存储另一种类型的对象,然后再检索它。 联合存在的唯一目的是类型双关我认为联合被添加到语言中是为了节省空间。 不,双关语是写一个成员,读另一个成员。写一个成员和读同一个成员不是双关语。写一个,读它,写第二个成员,读第二个成员也不是。当您读取与上次写入相同的成员时,您并没有更改类型,因此您没有绕过类型系统。 “类型双关语”通常被理解为表示作为一种类型写入,并以另一种类型读取相同的位。但是union 通常在struct 中使用,旁边还有一个enum,表示联合当前持有的类型。例如解释器可能有一个struct value,它可以包含一个整数一个浮点值,它可以有.type = T_INT.u.int_val = 123,或者.type = T_FLOAT.u.float_val = 4.56。在这种情况下,您只希望从 .u 中读取最初编写的相同类型,我认为这是“类型双关语”。 "我认为使用单个容器结构将不同类型的任意值存储在同一物理位置符合条件" 抱歉,但您这样做不 get 定义什么是“类型双关语”。这是一个古老且精确定义的概念(将对象的字节重新解释为另一种类型)。 重用未使用的存储来写入另一种类型的对象绝对是不是类型的双关语!

以上是关于是否通过 C99 中未指定的联合进行类型双关,并且它是否已在 C11 中指定?的主要内容,如果未能解决你的问题,请参考以下文章

使用带有联合的类型双关语的问题

什么是类型双关语,它的目的是什么?

memcpy 可以用于类型双关语吗?

C99 常量值传递

C99 标准是不是保证 unsigned int 的二进制表示?

如何将联合类型指定为对象键 Typescript