Jon*_* S. 5 c++ inheritance strict-aliasing language-lawyer reinterpret-cast
网站上已经有一些关于结构及其第一个成员变量以及结构及其第一个公共基数的指针可互换性的问题和答案。例如,这个问题就是其中之一。
然而,我感兴趣的不是非标准布局结构与其公共基础之间的未定义行为(reinterpret_cast或static_cast通过 a ),而是C++ 标准当前禁止此类强制转换的原因。现有的问题和答案不涉及这方面。void *
特别考虑以下示例(Godbolt):
#include <type_traits>
struct Base {
int m_base_var = 1;
};
struct Derived: public Base {
int m_derived_var = 2;
};
Derived g_derived;
constexpr Derived *g_pDerived = &g_derived;
constexpr Base *g_pBase = &g_derived;
constexpr void *g_pvDerived = &g_derived;
//These assertions all hold
static_assert(!std::is_pointer_interconvertible_base_of_v<Base, Derived>);
static_assert((void *)g_pDerived == (void *)g_pBase);
static_assert((void *)g_pDerived == g_pvDerived);
static_assert((void *)g_pBase == g_pvDerived);
//This is well-defined and returns &g_derived
Derived * getDerived() {
return static_cast<Derived *>(g_pvDerived);
}
//This is also well-defined; outer static_cast added to illustrate the sequence of conversions
Base * getBase() {
return static_cast<Base *>(static_cast<Derived *>(g_pvDerived));
}
//This is UB due to the first static_assert!
Base * getBaseUB() {
return static_cast<Base *>(g_pvDerived);
}
Run Code Online (Sandbox Code Playgroud)
正如您从 Godbolt 链接中看到的,所有三个函数在 x86-64 GCC 上编译为完全相同的程序集。然而,该标准禁止第三种变体,因为Base它不是 的指针可相互转换的基础Derived。
我的问题是:标准禁止这种类型转换是否有明显的原因?特别是,在我所知道的所有实现中,指向Base子对象的指针的值与指向整体的指针的值相同Derived,并且我没有看到为什么Derived不应再将其视为标准布局的特殊原因。(换句话说,Base位于 内的偏移量为零处。)C++ 实现在 内的非零偏移量处Derived放置是否合法?(是否已经有一个实现可以做到这一点?)BaseDerived
请注意,这个问题仅涉及没有虚拟成员函数/虚拟继承/多重继承的情况。
这实际上是两个问题:
什么是标准布局?
为什么指针互换性与标准布局相关联?
标准布局是根据 C++11 之前的“普通旧数据”类型概念的一半构建的。另一半是微不足道的可复制性(即:memcppy 对象的实例与复制构造函数一样好)。这两部分并没有真正相互作用,但 POD 两者都需要。
从纯粹标准 C++ 的角度来看,标准布局是构造其布局与现有类型匹配的类型的能力的要求,这样,如果将这两种类型推入 a 中union,则可以访问非活动成员的子对象。这是自 C++11 发明以来标准布局在该语言中启用的核心功能。
这就是为什么标准布局只允许类层次结构中的一个成员拥有非静态数据成员。如果只有一个类有 NSDM,那么不同基类的 NSDM 之间的顺序就不存在问题,等等。能够先验地知道该顺序对于能够知道两种类型匹配至关重要。
这对于跨语言交流也很有用。
然而,一旦定义了标准布局,它就开始被用于其领域中不太明确的部分。
例如,标准布局成为offsetof行为是否有效的决定因素。这是因为offsetof最初是基于 POD 类型,因此当布局部分被剥离时,offsetof更新为使用它。然而,这是次优的,因为唯一会破坏的基于布局的东西offsetof是虚拟基类(虚拟基类的成员的偏移量取决于最派生的类,而派生类又取决于对象的运行时类型)。现在,新的限制仍然比 POD更好,但它可以扩展以包含更多内容。但这意味着要提出一个新的定义。
类似的情况可能与指针可互换性有关。这个概念是在 C++17 中发明的,旨在解决对象模型的各种问题。论文中没有证据解释为什么他们选择标准布局来实现指针可互换性。但它是一个具有明确定义规则的现有工具,对于任何给定类型的“第一个子对象”已经有明确定义的规则。
根据需要扩展规则需要为“第一主题”创建新的定义。
基类指针可以相互转换为特定的派生类吗?那么,这取决于派生类继承自哪些其他类。第一个 NSDM 指针是否可以与其所属的类相互转换?这取决于继承图中还涉及哪些其他类。
这些依赖关系已经存在,但它们都脱离了特定的、预先存在的规则。您想要的需要创建一个更复杂的新规则。它将必须复制 90% 的现有标准布局规则(禁止virtual、公共/私有成员等),然后添加自己的规则。第一个 NSDM 是指针可相互转换的,除非任何基类非空。任何特定的基类只有在声明顺序中所有先前的基类都为空的 NSDM 时才可以进行指针互换。
仅仅依靠标准布局规则并说“标准布局类型与其第一个 NSDM 及其所有基类是指针可相互转换的”就容易多了。
额外的规则也会给规范带来一些负担。这是部分冗余,并且会产生错误。例如,C++23 有望通过取消混合public和private成员的禁止来扩展标准布局类型,强制布局严格按声明排序。如果指针互换性有自己的规则,则可以更新标准布局,但不能更新指针互换性。