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

时间:2010-10-10 22:39:41

标签: c++ struct c++11

3 个答案:

答案 0 :(得分:3)

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

,这是正确的行为
struct B : virtual A {};
struct C : virtual A {};

对于virtual,D已经不是第二条规则的标准布局类。这是C ++ '98,'03和'0x。

中的情况

编辑以反映评论:

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

标准布局类定义的要点是指定可以与其他语言一起使用的东西。我们以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的实例不兼容。限制可以防止出现这种情况。

答案 1 :(得分:3)

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

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

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

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


你说这是“禁止”一种可能性 - 从我的观点来看,它并不是真的令人生畏,它只是没有给那些最初没有这种类型的类型赋予“标准布局”状态(类似于碱基不是C ++中的POD 03)。所以它不是禁止这种类型,它只是说它们没有得到特殊的标准布局处理,它们首先得不到保证。


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

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

输出(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' failed.
Aborted

答案 2 :(得分:2)

  

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

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

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

假设A的定义如下:

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

如果允许A的两个副本共享一个地址,则此代码将无法按预期运行。我认为在这样的情况下,决定我们应该对A的不同副本有明显的补充。当然,这样的情况的奇怪使我避免多重继承。