具有“共同初始序列”的联合'双关'结构:为什么C(99+)而不是C ++规定了'联合类型的可见声明'?

时间:2016-01-05 16:06:10

标签: c++ c struct unions type-punning

背景

通过union讨论关于类型惩罚的大部分未实现或实现定义的性质通常引用以下位,通过@ecatmur(https://stackoverflow.com/a/31557852/2757035),对标准的豁免 - layout struct具有成员类型的“公共初始序列”:

  

C11( 6.5.2.3结构和联盟成员; 语义):

     
    

[...]如果一个union包含几个共享一个公共初始序列的结构(见下文),并且当前是一个union对象     包含这些结构之一,允许检查     其中任何一个的共同初始部分声明的任何地方     已完成的联合类型可见。两个结构共享一个     公共初始序列如果相应的成员具有兼容类型(并且对于位字段,相同的宽度),则序列为1或者     更多初始成员。

  
     

C ++ 03( [class.mem] / 16 ):

     
    

如果POD-union包含两个或多个共享公共初始序列的POD结构,并且POD-union对象当前包含一个     在这些POD结构中,允许检查共同的初始结构     他们中的任何一个。两个POD结构共享一个共同的初始序列     如果相应的成员具有布局兼容类型(并且,对于     比特字段,相同的宽度)用于一个或多个初始序列     成员。

  
     

这两个标准的其他版本都有类似的语言;从C ++ 11开始   使用的术语是标准布局而不是 POD

由于不需要重新解释,因此这不是真正的类型惩罚,只是应用于union成员访问的名称替换。 C ++ 17(臭名昭着的P0137R1)的提议使得这种语言明确地使用了“访问就像其他结构成员被提名一样”的语言。

但请注意粗体 - “在任何地方声明完整类型的联合” - C11中存在的条款但在2003年,2011年或2014年的C ++草案中没有任何内容(几乎完全相同,但后来的版本将“POD”替换为新术语标准布局)。在任何情况下,任何C ++标准的相应部分都完全没有'union类型位的可见声明。

@loop和@ Mints97,在这里 - https://stackoverflow.com/a/28528989/2757035 - 显示这一行在C89中也缺席,首先出现在C99 ,然后保留在C中(尽管如此,过滤到C ++)。

围绕此

的标准讨论

[剪掉 - 看我的回答]

问题

然后,我的问题是:

  • 这是什么意思?什么被归类为“可见声明”?该条款是否旨在缩小 - 或扩大 - 这种“惩罚”定义行为的背景范围?

  • 我们是否假设C ++中的这种遗漏是非常慎重的?

  • C ++与C不同的原因是什么? C ++是否只是从C89“继承”了这一点,然后决定 - 或者更糟,忘记 - 来与C99一起更新?

  • 如果差异是故意的,那么 C和C ++中的2种不同治疗有哪些好处或缺点?

  • 在编译或运行时有什么有趣的后果?例如,@ ecatmur,在回复我的评论中指出了他的原始答案(链接)如上所述),推测如下。

  

我认为它允许更积极的优化; C可以假设   函数参数S* sT* t即使共享一个,也不会别名   只要没有union { S; T; }在视图中的常见初始序列,   而C ++只能在链接时做出这个假设。可能值得   问一个关于这种差异的单独问题。

好吧,我在这里,问!我对这方面的任何想法都很感兴趣,特别是:(或者)标准的其他相关部分,委员会成员或其他受尊敬的评论员的引用,开发人员的见解,他们可能已经注意到由此引起的实际差异 - 假设任何编译器甚至 bothers 强制执行C的附加条款 - 等等。目的是生成有关此C子句及其(有意或无意)遗漏的相关事实的有用目录。那么,我们走了!

2 个答案:

答案 0 :(得分:17)

我已经通过迷宫找到了解决这方面一些重要来源的方法,我想我已经对它进行了非常全面的总结。我将此作为答案发布,因为它似乎解释了C子句的(IMO非常误导)意图以及C ++不继承它的事实。如果我发现进一步的支持材料或情况发生变化,这将随着时间的推移而发展。

这是我第一次尝试总结一个非常复杂的情况,即使对许多语言架构师来说这似乎也是不明确的,所以我欢迎有关如何改进这个答案的澄清/建议 - 或者只是一个更好的答案如果有人有。

最后,一些具体的评论

通过模糊相关的主题,我找到了@tab的以下答案 - 非常感谢所包含的链接(如果不是结论性的话)GCC和工作组缺陷报告:answer by tab on StackOverflow

GCC链接包含一些有趣的讨论,并揭示了委员会和编译器供应商的一部分相当多的混淆和相互矛盾的解释 - 围绕union成员struct的主题,讨论和别名在C和C ++中。

最后,我们将链接到主要事件 - 另一个BugZilla线程Bug 65892,其中包含极其有用的讨论。特别是,我们找到了两个关键文件中的第一个:

C99中添加行的来源

C proposal N685是有关union类型声明可见性的已添加子句的来源。通过一些声称(参见GCC线程#2)完全错误解释了共同的初始序列" N685确实旨在允许放宽#34;共同的初始序列"我们可以从这句引文中看到{T}知道某些struct包含所述union类型的实例的{0}}:

  

建议的解决方案是要求联合声明可见   如果通过一个共同的初始序列(如上所述)的别名是可能的。   因此,如果需要,以下TU提供这种别名:

struct

根据海湾合作委员会的讨论和评论,例如@ ecatmur,这个提案 - 似乎要求推测性地允许任何union utag { struct tag1 { int m1; double d2; } st1; struct tag2 { int m1; char c2; } st2; }; int similar_func(struct tag1 *pst2, struct tag2 *pst3) { pst2->m1 = 2; pst3->m1 = 0; /* might be an alias for pst2->m1 */ return pst2->m1; } 类型的别名,其中某些实例在某些struct内可见对于这个TU - 似乎受到了极大的嘲笑,很少被实施

很明显,如果不完全削弱许多优化措施来满足对附加条款的这种解释是多么困难 - 几乎没有什么好处,因为很少有程序员想要这种保证,而那些做的人只能开启{{ 1}}(IMO表示更大的问题)。如果实施,这种限额更有可能吸引人们并与union s的其他声明进行虚假互动,而不是有用。

从C ++

中省略该行

继续此以及我在其他地方发表的评论后,@Potatoswatter in this answer here on SO表示:

  

C ++中故意忽略了可见性部分,因为它被广泛认为是荒谬和无法实现的。

换句话说,看起来C ++故意避免采用这个附加条款,可能是由于它广泛存在的荒谬。在记录上要求""引用这一点,Potatoswatter提供了关于该线程参与者的以下关键信息:

  

讨论中的人基本上是#34;记录"那里。 Andrew Pinski是一个铁杆GCC后端人。 Martin Sebor是一名活跃的C委员会成员。 Jonathan Wakely是一位活跃的C ++委员会成员和语言/图书馆实施者。该页面比我能写的任何内容都更具权威性,清晰性和完整性。

Potatoswatter在上面链接的同一个SO线程中得出结论,C ++故意排除了这一行,没有对指向公共初始序列的指针进行特殊处理(或者,最好是实现定义的处理)。他们的待遇是否将在未来具体确定,与其他任何指针相比,还有待观察;与我下面关于C的最后一节比较。目前,它不是(并且IMO,这是好的)。

这对C ++和实际C实现意味着什么?

所以,有了N685的邪恶线......' 施放除了......我们回到假设指向公共初始序列的指针并不特别在别名方面。仍然。值得确认的是,没有它,C ++中的这一段意味着什么。好吧,上面的第二个GCC主题链接到另一个gem:

C++ defect 1719。此提案已达到 DRWP 状态:" DR问题,其解决方案反映在当前工作文件中。工作文件是该标准未来版本的草案。 - cite。这是在C ++之后的14或者至少在我在这里的最终草案之后(N3797) - 并且提出了一个重要的,并且在我看来有启发性,重写了这一段的措辞 , 如下。我正在考虑我认为重要的变化, {这些评论} 是我的:

  

在标准布局中与活跃成员结合 {"有效"表示fno-strict-aliasing实例,而不仅仅是类型} (9.5 [class.union])   在结构类型union中,允许读取 {以前"检查"} 非静态数据成员union   结构类型T1的另一个联盟成员的提供m是其中的一部分   常见的T2m的初始序列。 [注意:读取易失性对象   通过非易失性glvalue具有未定义的行为(7.1.6.1   [dcl.type.cv])。 - 后注]

这似乎澄清了旧措辞的含义:对我而言,它表示任何特别允许的' punning'具有共同初始序列的T1成员T2中必须通过父union 的实例完成 - 而不是基于类型struct的例子(例如指向它们的指针传递给某个函数)。这个措辞似乎排除了任何其他解释, a la N685。我会说,C会采取这种做法。嘿,说到哪,见下文!

结果是 - 正如@ecatmur和GCC门票所证明的那样 - 这在C ++中按照定义留下了这样的union成员structs,实际上在C中,受制于与任何其他2个正式无关的指针一样严格的别名规则。现在可以更清楚地定义能够读取非活动union成员struct的公共初始序列的明确保证,而不是包括模糊和难以想象的单调执行"可见性"当N685为C 尝试时。通过这个定义,主编译器的行为与C ++一样。至于C?

在C ++中的C /澄清

中可能逆转此行

同样非常值得注意的是,C委员会成员Martin Sebor也希望用这种优秀的语言来解决这个问题:

  

Martin Sebor 2015-04-27 14:57:16 UTC 如果你们中的一个人能用它来解释这个问题,我愿意写一篇论文并将其提交给WG14要求更改标准。

     

Martin Sebor 2015-05-13 16:02:41 UTC 上周我有机会与Clark Nelson讨论这个问题。 Clark过去曾致力于改进C规范的混叠部分,例如在N1520(http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1520.htm)中。他同意像N1520中指出的问题一样,这也是一个值得WG14重新审视和修复的突出问题。"

Potatoswatter鼓舞人心地总结道:

  

C和C ++委员会(通过Martin和Clark)将尝试找到共识并敲定措辞,以便标准最终能够说出它的意义。

我们只能希望!

再次欢迎所有进一步的想法。

答案 1 :(得分:6)

我怀疑这意味着不仅可以通过联合类型,而且可以在联合之外访问这些公共部分。也就是说,假设我们有这个:

union u {
  struct s1 m1;
  struct s2 m2;
};

现在假设在某个函数中我们有一个struct s1 *p1指针,我们知道这个指针是从这种联合的m1成员中解除的。我们可以将其转换为struct s2 *指针,并仍然可以访问与struct s1共有的成员。但是在范围的某处,必须能够看到union u的声明。它必须是完整的声明,通知编译器成员是struct s1struct s2

可能的意图是,如果范围中存在这样的类型,则编译器知道struct s1struct s2是别名的,因此通过struct s1 *指针进行访问是怀疑真正访问struct s2,反之亦然。

如果没有任何可见的联合类型以这种方式连接这些类型,就没有这样的知识;可以应用严格的别名。

由于C ++中没有措辞,那么为了利用该语言中的“常见初始成员放松”规则,您必须通过联合类型路由访问,这通常是通常的:

union u *ptr_any;
// ...
ptr_any->m1.common_initial_member = 42;
fun(ptr_any->m2.common_initial_member);  // pass 42 to fun