零大小的成员子对象。为什么不?

时间:2010-10-12 11:34:36

标签: c++

这是今天零大小对象和子对象系列中的第三个问题。 标准明确暗示成员子对象不能具有零大小,而基类子对象可以。

struct X {}; //empty class, complete objects of class X have nonzero size
struct Y:X { char c; }; //Y's size may be 1 
struct Z {X x; char c;}; //Z's size MUST be greater than 1

为什么不允许零大小的成员子对象只是像零大小的基类子对象一样?

TIA

康拉德回答后的编辑: 请考虑以下示例:

struct X{}; 
struct D1:X{};
struct D2:D1, X {}; //D2 has 2 distinct subobjects of type X, can they both be 0 size and located at the same address?

如果两个相同类型X的基类子对象(如我的示例中)可以位于同一地址,那么应该能够成员子对象。如果他们不能,那么编译器会特别处理这种情况,因此它可以特别处理Konrad的示例(请参阅下面的答案),如果同一类中有多个相同类型,则不允许零大小成员子对象。我哪里错了?

3 个答案:

答案 0 :(得分:10)

  

为什么不允许零大小的成员子对象

因为相同类型的几个(子)对象可以具有相同的地址,并且标准禁止

struct X { virtual ~X() { /* Just so we can use typeid! */ } };
struct Y {
    X a;
    X b;
};

Y y;
// The standard requires that the following holds:
assert(typeid(y.a) != typeid(y.b) or &y.a != &y.b);

这有点合乎逻辑:否则,对于所有意图和目的,这两个对象将相同(因为对象的标识仅由其类型和内存地址决定)并单独声明它们毫无意义。

答案 1 :(得分:2)

我认为你的问题很好 - 事实上,一些关于空基类优化的早期文章谈到了“空成员优化”,并特别指出类似的优化可以适用于成员。也许,在有标准之前,有些编译器会这样做。

这只是空闲的猜测,我没有多少支持它,但我昨天看了一些标准的这些方面。

C兼容性

在这个例子中:

struct X{};
struct Y{};
struct Z {
   struct X x;
   struct Y y;
   int i;
};

Z将是C ++ 03规则下的POD,但如果xy为零大小的子对象,则不会与C布局兼容。 C布局兼容性是POD存在的原因之一。基类不会发生这个问题,因为C没有基类,而且基类的类不是C ++ 03中的POD,所以所有的注意都是关闭的:)。访问者注意到C事实上,它不支持空结构。所以这整个论点都是错误的。我只是假设它确实 - 它似乎是一种无害的概括。

此外,程序似乎假设事物,如y具有比x更大的地址,以及类似的东西 - 这是由5.10 / 2中指针的关系运算符保证的。我真的不知道是否有令人信服的理由允许这样做,或者有多少程序在实践中使用它。

IMO,这是所有这些中最强烈的论据。

不能很好地概括为数组

继续上面的例子,添加:

struct Z1 {
   struct X x[1];
   struct Y y[1];
   int i;
};

...人们可能期望sizeof(Z1) == sizeof(Z),而xy也可以像普通数组那样工作(即你可以形成一个过去的指针,并且该指针与任何元素的地址不同)。其中一个期望将被零大小的子对象打破。

比基本案例

更不引人注目

从空基础派生的主要原因之一是因为它是策略或接口类型类。这些通常是空的,并且要求它们占用空间会产生“抽象惩罚”,即使更好的组织代码更加膨胀。这是Stroustrup在C ++中不需要的东西 - 他希望以最小的运行时成本进行适当的抽象。

另一方面,如果声明一个类型的成员,则不继承其函数,typedef等;并且你没有得到从派生到基数的特殊指针转换,所以也许没有理由拥有零大小的成员而不是零大小的基础。

此处的反例类似于STL容器中的Allocator策略类 - 您不一定希望容器从它派生,但是您希望“保持它”,而不会占用开销。

空基类案例涵盖了大多数用途

...如果您担心空间开销,可以使用私有继承而不是声明成员。它不是那么直接,但你可以或多或少地达到同样的目的。显然,如果你有很多空的成员想要占用零空间,那么这种方法效果不会很好。

这是另一个特例

有一些微妙的东西不适合这种优化。例如,您不能将memcpy零大小的POD子对象的位char转换为operator=数组,然后返回,或者在零大小的子对象之间。我见过人们使用memcpy实现{{1}}(我不知道为什么......)会破坏这种事情。据推测,为基类而不是成员打破这些事情并不是一个问题。

答案 2 :(得分:1)

编译器可以有条件地允许零大小的成员对象以及基类,当然,但它会更复杂。无论类型如何,空基类优化始终都适用。每当编译器看到一个类派生自没有数据成员的类时,它就可以使用空基类优化。

关注@Konrad Rudolphs示例,使用成员对象,它必须检查类型,验证该位置是否存在相同类型的其他对象,然后可能会应用您的优化。好吧,除非成员对象位于包含类的末尾。如果是这样,那么对象的“真实”(非零)大小将突出到包含类的末尾,这也是一个错误。这种情况永远不会发生在基类中,因为我们知道基类位于派生类的开头,派生类的大小非零。

因此,这样的优化会更复杂,更微妙,并且更有可能以意想不到的方式中断。

我不能引用零大小的成员对象肯定中断的任何情况,但我不相信它们也不存在。我已经指出了基类案例中不存在的一些限制,并且很可能存在更多限制。所以问题是,语言允许多少复杂性和不确定性只是为了使一个很少有用的优化成为可能