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

背景

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

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

[…]如果一个union包含几个共享一个公共初始序列的结构(见下文),并且如果union对象当前包含这些结构中的一个,则允许检查它们中任何一个的公共初始部分。 可以看到已完成的工会类型的声明 。 如果对应的成员具有一个或多个初始成员的序列的兼容类型(并且对于位字段,具有相同的宽度),则两个结构共享共同的初始序列

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“inheritance”了这个,然后决定 – 或者更糟, 忘记 – 与C99一起更新?

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

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

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

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

我已经通过迷宫找到了解决这个问题的方法,我想我已经对它进行了非常全面的总结。 我发布这个作为答案,因为它似乎解释了C语句的(IMO非常误导)意图和C ++不inheritance它的事实。 如果我发现进一步的支持材料或情况发生变化,这将随着时间的推移而演变。

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

最后,一些具体的评论

通过模糊相关的线程,我找到了@tab的以下答案 – 非常感谢包含的链接(有启发性,如果不是结论性)GCC和工作组缺陷报告: 在StackOverflow上通过选项卡回答

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

最后,我们链接到主要事件 – 另一个BugZilla线程, 错误65892 ,包含一个非常有用的讨论。 特别是,我们找到了两个关键文件中的第一个:

C99中添加行的来源

C提案N685是关于union类型声明可见性的附加条款的起源。 通过一些声称(参见GCC线程#2)对“公共初始序列”允许的完全误解,N685确实旨在允许放宽TU内的“公共初始序列” struct的别名规则,意识到包含一些union所述struct类型的实例 ,正如我们从这句话中看到的:

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

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

根据海湾合作委员会的讨论和下面的评论,如@ ecatmur’s,这个提议 – 似乎要求推测性地允许任何struct类型的别名,在某个union中有一些实例对这个TU可见 – 似乎已经受到很大的嘲笑并且很少被实现

显而易见的是,如果没有完全削弱许多优化措施来满足对附加条款的这种解释是多么困难 – 几乎没有什么好处,因为很少有编码人员想要这种保证,而那些做的人只能打开fno-strict-aliasing (IMO指示更大的问题)。 如果实施,这种限额更有可能将人们赶出并与union的其他声明进行虚假互动,而不是有用。

从C ++中省略该行

继此之后,我在其他地方做了一个评论, @ Potatoswatter在这里的答案中指出:

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

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

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

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

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

所以,有了N685的邪恶线……“抛开”…我们回过头来假设指向共同的初始序列在别名方面并不特别。 仍然。 值得确认的是,没有它,C ++中的这一段意味着什么。 好吧,上面的第二个GCC线程链接到另一个gem:

C ++缺陷1719 。 该提案已达到DRWP状态:“DR问题的解决方案反映在当前的工作文件中。工作文件是该标准未来版本的草案” – 引用 。 这是在C ++之后的14或至少在我在这里的最终草案之后(N3797) – 并且提出了一个重要的,并且在我看来有启发性地重写了这一段的措辞 ,如下所述。 我正在强调我认为是重要的变化, {这些评论}是我的:

具有活动成员的标准布局联合中 {“active”表示结构类型T1 union实例,而不仅仅是类型} (9.5 [class.union]),允许读取 {以前“检查”}非结构结构类型T2 的另一个union成员的静态数据成员m提供的mT1T2的公共初始序列的一部分。 [ 注意 :通过非易失性glvalue读取volatile对象具有未定义的行为(7.1.6.1 [dcl.type.cv])。 – 尾注]

这似乎澄清了旧措辞的含义:对我来说,它说任何特殊允许的“惩罚”在具有共同初始序列的union成员struct必须通过union 的实例来完成 – 而不是基于类型structs (例如指向它们的指针传递给某个函数)。 这个措辞似乎排除了任何其他解释, N685。 我会说,C会采取这种做法。 嘿,说到哪,见下文!

结果是 – 正如@ecatmur和GCC门票所certificate的那样 – 这样就可以在C ++中按照定义留下这样的union成员struct ,实际上在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)将尝试找到共识并敲定措辞,以便标准最终可以说出它意味着什么。

我们只能希望!

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

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

 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