【问题标题】:"no base classes of the same type as the first non-static data member"“没有与第一个非静态数据成员相同类型的基类”
【发布时间】:2010-10-10 22:39:41
【问题描述】:

我在 comp.std.c++ 上asked this a while ago 并没有得到回复。

我只是在稍作修改的情况下引用我的帖子。


标准布局类的最后一个要求,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

在 5.10 的上下文中,子对象仅在 成员指针的比较要求。基础子对象是 无关的。此外,它不会使 给一个(标量)指针到一个 成员子对象和指向高于的基础子对象的指针 比较指向基本子对象的指针。

C++03 中没有这样的限制。即使有一个 ABI 要求每个成员都 分配在与任何相同类型的基址不同的地址,但 已经允许对上面的代码进行空基类优化,我 认为 ABI 有问题,标准不应该捕捉到这一点。

语言goes back to N2172 这表明多重继承可能会引起麻烦和需要 在标准布局类中不允许使用以确保ABI compatibility; 但是,这最终是允许的,因此要求 没有意义。


供参考,1.8/5-6:

5 除非它是位域 (9.6),否则 大多数派生对象应具有 非零大小,应占用一个或 更多字节的存储空间。基类 子对象的大小可能为零。一个 可简单复制的对象或 标准布局类型 (3.9) 应 占用连续字节的存储空间。

6 除非对象是位域或 零大小的基类子对象, 该对象的地址是地址 它占用的第一个字节。二 两者都不是的不同对象 位域或基类子对象 大小为零的应具有不同的 地址。

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

补充说明:

10.1/8 指的是与 5.10 相同的神秘内容,但它也只是一个信息说明。

[注意:……基类子对象的大小可能为零(第 9 条);但是,两个具有相同类类型且属于同一个最派生对象的子对象不能分配在同一地址(5.10)。 ——尾注]

GCC 似乎保证相同类型的空基本子对象被赋予唯一地址。 Example program and output. 这似乎足以保证给定类型的对象由地址唯一标识。这将超出 C++ 对象模型§1.8 的保证范围。当然这是个好主意,但标准似乎没有要求。同样,平台 ABI 可以将此保证扩展到第一个成员别名为空基的类。该语言设定了 ABI 的最低要求;一个 ABI 可以添加语言特性,其他 ABI 也可以效仿,标准的追赶过程很容易出错。

我的问题是,给定的要求是否在标准的上下文中完成了任何事情,而不是与其他 ABI 保证一起对用户是否有用。证明这种唯一地址保证是有意的并且只是偶然省略的证据,也会使要求更有意义。


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

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

实际上,只要包含空基类优化,该要求就允许实现继续使用现有 ABI。现有编译器可能会在违反要求时禁用 EBO,以避免基地址与第一个成员的地址重合。如果标准没有以这种方式限制程序,库和程序将不得不使用更新的 C++0x 编译器重新编译……不值得!

【问题讨论】:

    标签: c++ struct c++11


    【解决方案1】:

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

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

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

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


    您是说这是“禁止”一种可能性 - 从我的角度来看,这并不是真正禁止,它只是没有将“标准布局”状态授予原本没有的类型(具有碱基不是 C++03 中的 POD)。所以这并不是禁止这种类型,只是说它们没有得到特殊的标准布局处理,它们一开始就没有得到保证。


    关于我的断言,非静态数据成员子对象和基础子对象是不同的,看看你是否觉得这令人信服:

    • 5.9/2(指针上的关系运算符)清楚地表明,没有两个数据成员子对象(至少,具有相同的访问说明符)彼此具有相同的地址。
    • 5.3.1/1(一元运算符*)说“应用它的表达式应该是一个指向对象类型 [snip] 的指针,结果是一个引用 the 的左值表达式指向的对象。” (强调)这意味着在给定时间,在特定地址上最多有一个给定类型的对象。
    • 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
    }
    

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

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

    【讨论】:

    • 相关点是 9.2/19,“指向标准布局结构对象的指针,使用 reinterpret_cast 适当转换,指向其初始成员……”。这可能是导致我最初说“……必须产生两个实例……”的原因。但是,基类的放置是实现定义的,并且空基类可以放在任何地方,正如 GCC 的解决方法所展示的那样。 (ideone.com/Zy4qj) 如果初始成员与空基址共享其类型,GCC 可以应用与另一个相同类型的间接基址相同的解决方法,并将基址提升到更高的地址。
    • @Potatoswatter:是的,我认为提高基类的位置是一个合适的解决方法,但它会在基本上所有实现中引入 ABI 特殊情况和向后兼容性破坏。对于一个特殊情况,委员会可能认为额外的麻烦不值得。
    • @Doug:提升没有存储空间的 empty 基类的位置。正如我所指出的,GCC 已经这样做了。 (我刚刚更新了 ideone 链接来演示这个案例。)完全禁止某些东西如何比任何其他替代方案提供更多的向后兼容性?
    • “但是,正如其他答案所解释的那样,属于同一完整对象的所有基类子对象和成员子对象必须是不同的,即如果它们属于同类型。” — 如果你能证明这不仅仅是一个特定于平台的 ABI 扩展,我肯定会选择你的答案作为接受。
    • 我认为你已经走上了正轨。 1) 现在需要空基类优化,而 C++03 POD 结构不能有基类。 2)保证空基地和第一个成员之间的唯一地址需要增加基地。 (这通常独立于相同类型的唯一地址。) 3)撞上空基地可能不像“标准”EBO那样普遍实施。在这种情况下,某些平台可能只是禁用 EBO,这将违反标准布局。 ——所以你基本上明白了……去掉多余的断言,我会选择这个答案。
    【解决方案2】:

    如果您将该等式表达式放在assert() 中,您会发现它失败了。 A 子对象位于不同的位置。这是不指定virtual的正确行为:

    struct B : virtual A {};
    struct C : virtual A {};
    

    对于virtual,根据第二条规则,D 已经不是标准布局类。在 C++ '98、'03 和 '0x 中就是这种情况。

    编辑以反映 cmets:

    再次编辑:没关系,这还不够。

    标准布局类定义的重点是指定可以与其他语言一起使用的东西。让我们以 C 为例。一般情况下,如下C++类

    struct X : public B{
      B b;
      int i;
    };
    

    相当于这个 C 结构:

    struct X{
      B base;
      B b;
      int i;
    };
    

    如果 B 是一个空类并且应用了空基优化,那么 X 在 C 中将等价于:

    struct X{
      B b;
      int i;
    };
    

    但是,交互的 C 端不会知道该优化。 C++ X 和 C X 的实例不兼容。该限制阻止了这种情况。

    【讨论】:

    • 你能提供一个参考,说明这是必需的行为吗?
    • 10.1 [class.mi],第 4 段:“不包含关键字 virtual 的基类说明符指定非虚拟基类。包含关键字 virtual 的基类说明符指定一个虚拟基类。对于非虚拟基类在最派生类的类格中的每次不同出现,最派生对象(1.8)应包含该类型的相应不同基类子对象。对于每个不同的基类如果指定为虚拟,则最派生的对象应包含该类型的单个基类子对象。"
    • @Steve:查看更新问题中的引用。特别是,允许​​空的基础子对象具有相同的地址:“两个不同的对象,既不是位字段也不是零大小的基类子对象,应具有不同的地址。”
    • @Potatoswatter:它们可以占用0字节,但不是必须的。在这种情况下,它们必须位于不同的地址(因此至少占用一个字节),因为非虚拟继承会导致两个不同的子对象。
    • 重新更新:尽管如此,标准布局类 struct X : public C { B b; int i; }; 并不等同于 C 结构 struct X{ C base; B b; int i; };,因为空基优化适用于任何一种方式。如果您希望布局匹配,请不要使用继承。
    【解决方案3】:

    具有公共基类的两个空基类必须在同一地址产生两个基类实例。

    我不这么认为。事实上,快速检查我的 g++ 副本表明我有两个不同的 A 对象地址。 IE。您上面的代码不正确。

    事实上,按照类的编写方式,我们必须有两个 A 对象。如果两个对象共享相同的地址,它们在任何有意义的意义上都不是两个不同的对象。因此,要求 A 对象的实例存在不同的地址。

    假设 A 是这样定义的:

    class A
    {
       static std::set<A*> instances;
       A() { instances.insert(this); }
       ~A() { instances.remove(this); }
    }
    

    如果允许 A 的两个副本共享一个地址,则此代码将无法按预期运行。我相信正是在这样的情况下,我们才决定我们应该为 A 的不同副本有不同的地址。当然,正是这种情况的怪异使我避免了多重继承。

    【讨论】:

    • 同一地址的两个对象是不同的,因为它们都是构造的,可能带有不同的参数,并且都被销毁了。我找不到我在哪里得到保证,但至少允许它们位于同一个地址,因为 §1.8/5 说“基类子对象的大小可能为零。”
    • 我没有看到“基类子对象的大小可能为零。”意味着允许相同类型的两个子对象共享一个地址。在我看来,意图是 B 和它的 A 子对象可以共享一个地址。我认为当我们考虑两个相同类型的对象时,它们必须具有不同的地址。
    • @Winston:请参阅更新问题中的 FCD 报价。特别是“既不是位字段也不是零大小的基类子对象的两个不同对象应具有不同的地址。”
    • 关于更新:允许,但这里的目的是决定语言支持什么……
    • 你的问题是:标准布局类的最后一个要求,9/6,是必要的还是有用的?因此我添加了那个。
    最近更新 更多