union 'punning' structs w/ "common initial sequence":为什么 C (99+) 而不是 C++ 规定了 'visible declaration of the union type'?

union 'punning' structs w/ "common initial sequence": Why does C (99+), but not C++, stipulate a 'visible declaration of the union type'?

背景

通过 union 关于类型双关的主要未定义或实现定义的性质的讨论通常引用以下位,此处来自 @ecatmur ( ),关于豁免具有 "common initial sequence" 成员类型的标准布局 struct

C11 (6.5.2.3 Structure and union members; Semantics):

[...] if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

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

If a POD-union contains two or more POD-structs that share a common initial sequence, and if the POD-union object currently contains one of these POD-structs, it is permitted to inspect the common initial part of any of them. Two POD-structs share a common initial sequence if corresponding members have layout-compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

这两个标准的其他版本具有相似的语言;自 C++11 使用的术语是 standard-layout 而不是 POD.

由于不需要重新解释,这并不是真正的类型双关,只是将名称替换应用于 union 成员访问。 C++17 的提案(臭名昭著的 P0137R1)使用 'the access is as if the other struct member was nominated'.

这样的语言明确了这一点

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

@loop 和@Mints97,这里 - - 显示此行在 C89 中也 不存在,首先出现在 C99 中,此后保留在 C 中(不过,再一次,永远不要过滤到 C++)。

围绕此的标准讨论

[已截取 - 看我的回答]

问题

由此,我的问题是:

I'd imagine it permits more aggressive optimization; C can assume that function arguments S* s and T* t do not alias even if they share a common initial sequence as long as no union { S; T; } is in view, while C++ can make that assumption only at link time. Might be worth asking a separate question about that difference.

好吧,我来问了!我对关于此的任何想法都非常感兴趣,尤其是:(任一)标准的其他相关部分,委员会成员或其他受人尊敬的评论员的引述,可能已经注意到实际差异的开发人员的见解 - 假设任何编译器甚至bothers 强制执行 C 的添加子句 - 等等。目的是生成有关此 C 子句及其(有意或无意)从 C++ 中遗漏的相关事实的有用目录。那么,我们走吧!

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

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++ 中没有这种措辞,因此要利用该语言中的 "common initial members relaxation" 规则,您必须通过联合类型路由访问,就像通常所做的那样:

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

我在迷宫中找到了一些很好的资源,我想我已经对它有了一个相当全面的总结。我 post 将此作为答案,因为它似乎解释了 C 子句的(IMO 非常误导的)意图和 C++ 不继承它的事实。如果我发现进一步支持 material 或情况发生变化,这将随着时间的推移而发展。

这是我第一次尝试总结一个非常复杂的情况,甚至对许多语言架构师来说似乎 ill-defined,所以我欢迎 clarifications/suggestions 如何改进这个答案 - 或者如果有人有一个更好的答案。

最后,一些具体的评论

通过模糊相关的线程,我找到了@tab 的以下答案 - 非常感谢所包含的 links 到(说明性的,如果不是决定性的)GCC 和工作组缺陷报告:answer by tab on Whosebug

GCC link 包含一些有趣的讨论,并揭示了部分委员会和编译器供应商的大量混淆和相互矛盾的解释 - 围绕 union 成员 struct 的主题C 和 C++ 中的 s、双关语和别名。

最后,我们 link 进入了主要活动 - 另一个 BugZilla 线程,Bug 65892,其中包含 非常 有用的讨论.特别是,我们找到了两个关键文档中的第一个:

C99 添加行的来源

C proposal N685 是关于 union 类型声明可见性的添加子句的来源。通过一些声称(参见 GCC 线程 #2)是对 "common initial sequence" 津贴的完全误解,N685 确实 旨在允许放宽 "common initial sequence" struct 的别名规则s 在一个 TU 中知道一些 union 包含所述 struct 类型 的实例,正如我们从这个引用中看到的那样:

The proposed solution is to require that a union declaration be visible if aliases through a common initial sequence (like the above) are possible. Therefore the following TU provides this kind of aliasing if desired:

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

根据 GCC 的讨论和下面的评论(例如 @ecatmur 的评论)判断,该提案似乎要求推测性地允许任何 struct 类型的别名,该类型在某些 union 可见的实例中恩 - 似乎受到了很大的嘲笑,很少被实施

很明显,在不完全破坏许多优化的情况下满足对添加子句的这种解释是多么困难 - 几乎没有好处,因为很少有编码人员想要这种保证,而那些想要这种保证的人只需打开 fno-strict-aliasing(IMO 表示更大的问题)。如果实施,这个允许更有可能把人抓出来并虚假地与 union 的其他声明交互,而不是有用。

省略来自 C++ 的行

根据这篇文章和我在其他地方发表的评论,@Potatoswatter in this answer here on SO 指出:

The visibility part was purposely omitted from C++ because it's widely considered to be ludicrous and unimplementable.

换句话说,看起来 C++ 故意避免采用这个添加的子句,可能是因为它被广泛认为是荒谬的。 在要求 "on the record" 引用这个, Potatoswatter 提供了以下有关线程参与者的关键信息:

The folks in that discussion are essentially "on the record" there. Andrew Pinski is a hardcore GCC backend guy. Martin Sebor is an active C committee member. Jonathan Wakely is an active C++ committee member and language/library implementer. That page is more authoritative, clear, and complete than anything I could write.

Potatoswatter,在上面的同一个 SO 线程 linked 中得出结论,C++ 故意排除了这一行,没有对指向公共的指针进行特殊处理(或者充其量,implementation-defined 处理)初始序列。他们的治疗是否会在未来被具体定义,而不是任何其他指针,还有待观察;与我下面关于 C 的最后一节进行比较。不过目前,它不是(再一次,IMO,这很好)。

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

所以,随着来自 N685 的恶意行...'cast aside'...我们回到假设指向公共初始序列的指针在术语上并不特殊别名。仍然。如果没有它,C++ 中的这一段意味着什么是值得确认的。好吧,link 上面的第二个 GCC 线程到另一个 gem:

C++ defect 1719. This proposal has reached DRWP status: "A DR issue whose resolution is reflected in the current Working Paper. The Working Paper is a draft for a future version of the Standard" - cite。这要么是 post C++14,要么至少是在我这里的最终草案 (N3797) 之后 - 并提出了 重要的,并且在我看来具有启发性的重写本段的措辞,如下。我将我认为重要的更改加粗,{这些评论} 是我的:

In a standard-layout union with an active member {"active" indicates a union instance, not just type} (9.5 [class.union]) of struct type T1, it is permitted to read {formerly "inspect"} a non-static data member m of another union member of struct type T2 provided m is part of the common initial sequence of T1 and T2. [Note: Reading a volatile object through a non-volatile glvalue has undefined behavior (7.1.6.1 [dcl.type.cv]). —end note]

这似乎澄清了旧措辞的含义:对我来说,它说 union 成员 struct 中任何特别允许的 'punning' 具有共同点初始序列必须 通过父 union 的实例 完成 - 而不是基于 structs 的类型(例如指向它们传递给了某个函数)。这种写法似乎排除了任何其他解释,a la N685。我会说,C 会很好地采用它。哎,说起来,往下看!

结果是——正如@ecatmur 和 GCC 门票中很好地展示的那样——根据 C++ 中的定义,这留下了 这样的 union 成员 struct,实际上在C,与任何其他 2 个官方无关的指针一样,遵守相同的严格别名规则. 能够读取非活动 union 成员 struct 的公共初始序列的明确保证现在定义得更清楚,不包括模糊和难以想象的 tedious-to-enforce "visibility",因为 N685 尝试 用于 C。根据这个定义,主要编译器的行为一直符合 C++ 的预期。至于C?

C 中这条线的可能反转/C++ 中的澄清

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

Martin Sebor 2015-04-27 14:57:16 UTC If one of you can explain the problem with it I'm willing to write up a paper and submit it to WG14 and request to have the standard changed.

Martin Sebor 2015-05-13 16:02:41 UTC I had a chance to discuss this issue with Clark Nelson last week. Clark has worked on improving the aliasing parts of the C specification in the past, for example in N1520 (http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1520.htm). He agreed that like the issues pointed out in N1520, this is also an outstanding problem that would be worth for WG14 to revisit and fix."

Potatoswatter 鼓舞人心地总结道:

The C and C++ committees (via Martin and Clark) will try to find a consensus and hammer out wording so the standard can finally say what it means.

我们只能希望!

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