"没有与第一个非静态数据成员相同类型的基类"

Pot*_*ter 5 c++ struct c++11

刚才在comp.std.c ++上问了这个问题并没有回复.

我只想在那里引用我的帖子,几乎没有修改.


标准布局类9/6的最后一个要求是必要的还是有用的?

提供了脚注说明:

这可确保具有相同类类型且属于同一最派生对象的两个子对象不在同一地址(5.10)分配.

单独看,脚注是不正确的.具有公共基类的两个空基类可以在同一地址处生成基类的两个实例.

struct A {};
struct B : A {};
struct C : A {};
struct D : B, C {};

D d;
static_cast<A*>(static_cast<B*>(&d))
   == static_cast<A*>(static_cast<C*>(&d)); // allowed per 1.8/5
Run Code Online (Sandbox Code Playgroud)

在5.10的上下文中,子对象仅在指向成员的指针的比较要求中提及.基础子对象无关紧要.此外,对于指向成员子对象的(标量)指针和指向基础子对象之间的指针之间的比较,在基本子对象的指针之间进行比较之间进行比较是没有意义的.

在C++ 03中没有这样的限制.即使有一个ABI需要每个成员分配在同一类型的任何基地的不同地址,但已经允许对上述代码进行空基类优化,我认为ABI是错误的,标准应该是抓住这个.

语言可以追溯到N2172 ,这表明多重继承可能会导致麻烦,需要在标准布局类中禁止它以确保ABI兼容性 ; 然而,这最终是允许的,并且从那个角度来看,这个要求没有意义.


供参考,1.8/5-6:

5除非它是一个位域(9.6),否则一个派生最多的对象应具有非零大小,并且应占用一个或多个存储字节.基类子对象可以具有零大小.平凡可复制或标准布局类型(3.9)的对象应占用连续的存储字节.

6除非对象是零字段或零大小的基类子对象,否则该对象的地址是它占用的第一个字节的地址.两个不同的对象既不是位字段也不是零大小的基类子对象应具有不同的地址.

(脚注)在"as-if"规则下,如果程序无法观察到差异,则允许实现在同一机器地址存储两个对象,或者根本不存储对象.

补充说明:

10.1/8指的是5.10中相同的神秘内容,但它也只是一个信息性的注释.

[注意:......基类子对象的大小可以为零(第9条); 但是,必须不在同一地址(5.10)分配具有相同类类型且属于同一最多派生对象的两个子对象. - 结束说明]

GCC似乎保证给相同类型的空基子对象赋予唯一的地址.示例程序和输出.这似乎足以保证给定类型的对象由地址唯一标识.这将超出C++对象模型§1.8的保证范围.当然这是个好主意,但标准似乎并不需要.同样,平台ABI可以将此保证扩展到一个类,其中第一个成员别名为空基.该语言规定了ABI的最低要求; ABI可以添加语言功能,其他ABI也可以效仿,标准的追赶过程只是容易出错.

我的问题是给定的要求是否在标准的上下文中完成了任何事情,而不是它是否对用户与其他ABI保证一致有用.有证据表明这种独特地址保证是有意的,并且只是偶然遗漏,这也使得这一要求更有意义.


总结答案(或我的结论,无论如何):

该要求在理论上并不能确保任何事情,因为无论如何都可以确保给定类型的所有对象具有不同的地址.当空基类子对象的地址与另一个对象(另一个基础或成员)冲突时,编译器可以简单地在结构中为其分配任意位置.由于标准布局规则仅描述数据成员的位置(可能是继承的),因此空基的位置仍未指定,并且可能在类似的标准布局类之间不兼容.(就我所注意到的那样,非空基地的位置仍然没有说明,然而在这种情况下,"第一个成员"的意思并不清楚,但在任何情况下它们都必须一致.)

在实践中,该要求允许实现继续使用现有的ABI,只要它们包括空基类优化即可.当违反要求时,现有编译器可以禁用EBO,以避免基址的地址与第一个成员的地址一致.如果标准没有以这种方式限制程序,则必须使用更新的C++ 0x编译器重新编译库和程序......不值得!

Dou*_*oug 3

标准布局类的“特殊能力”之一是,您可以将reinterpret_cast指向标准布局类对象的指针指向其第一个数据成员的类型,从而获得指向第一个数据成员的指针。[编辑:9.2/19] 此外,具有非静态数据成员的标准布局类允许具有空基数。您无疑知道,大多数实现将基类子对象放在完整子对象的开头。这种限制的组合有效地要求将空基类优化应用于标准布局类的所有基。

然而,正如其他答案所解释的,属于同一完整对象的所有基类子对象和成员子对象必须是不同的,即,如果它们属于相同类型,则具有不同的地址。违反您的要点的类(具有与第一个成员类型相同的基类)不能完全应用空基类优化,因此如果基类不能是标准布局类位于完整对象的开头。

所以我很确定这就是它的意思 - 它试图说“如果一个类有基类,并且不能应用空基类优化,那么该类不是标准布局”。

编辑:我在这里对术语有点松懈 - 可以构建空基类优化无法在基类之间完全应用的情况(例如,在您的struct D)中,但这并不重要,因为基类仍然可以从对象的开头开始,并在概念上“覆盖”数据成员,类似于union. 正如您所说,如果基础子对象(或基础)会覆盖另一个基础,则基础子对象的地址会增加。虽然同样的事情有可能发生在标准布局案例的基础上(如果它们与相同类型的数据成员重叠),但这会破坏现有的 ABI,并添加特殊情况而几乎没有什么好处。


你是说这是“禁止”一种可能性 - 从我的角度来看,这并不是真正的禁止,它只是不向最初不具有这种状态的类型授予“标准布局”状态(带有基数的类不是C++03 中的 POD)。因此,它并不是禁止此类类型,只是说它们没有得到特殊的标准布局处理,而这些处理一开始就没有得到保证。


关于我关于非静态数据成员子对象和基本子对象是不同的断言,看看您是否认为这令人信服:

  • 5.9/2(指针上的关系运算符)明确指出,没有两个数据成员子对象(至少具有相同的访问说明符)具有彼此相同的地址。
  • 5.3.1/1(一元运算符*)表示“应用它的表达式应是指向对象类型 [snip] 的指针,结果是引用表达式指向的对象的左值。(强调)这意味着在给定时间的特定地址最多有一个给定类型的对象。
  • 1.8/2 “子对象可以是成员子对象(9.2)、基类子对象(第 10 条)或数组元素。”...我认为这意味着类别是互斥的(即使它们的存储重叠) 。标准的其他部分非常强烈地暗示基础子对象和成员子对象是不同的(例如12.6.2)。
  • Steve M 对 10.1/4 的引用“对于最派生类的类格中非虚拟基类的每次不同出现,最派生对象 (1.8) 应包含该类型的相应不同基类子对象。” - 我相信这意味着不同的基数必须位于不同的地址,否则它们就不会是“不同的”对象 - 在它们的共同生命周期中将无法区分它们。

如果您不认为脚注是规范性的或充分表明意图的话,我不知道这有多令人信服。无论如何,Stroustrup 在《C++ 编程语言》12.2 中根据成员对象解释了派生类,这些成员对象具有编译器支持的从派生到基类的转换。事实上,在本节的最后,他明确地说:“使用类作为基类相当于声明该类的(未命名)对象。因此,必须定义一个类才能将其用作基类(第 5.7 节)。”


另外:在这种特定情况下,GCC 4.5 似乎不会提高基类,即使它确实提高了重复基类的基础(如您所示):

#include <assert.h>
#include <iostream>

struct E {};
struct D: E { E x ; };

int main()
{
   D d;
   std::cerr << "&d: " << (void*)(&d) << "\n";
   std::cerr << "&d.x: " << (void*)(&(d.x)) << "\n";
   std::cerr << "(E*)&d: " << (void*)(E*)(&d) << "\n";
   assert(reinterpret_cast<E *>(&d) == &d.x); //standard-layout requirement
}
Run Code Online (Sandbox Code Playgroud)

输出(Linux x86-64,GCC 4.5.0):

&d: 0x7fffc76c9420
&d.x: 0x7fffc76c9421
(E*)&d: 0x7fffc76c9420
testLayout: testLayout.cpp:19: int main(): 断言 `reinterpret_cast(&d) == &d.x' 失败。
中止