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

C++ 零大小的成员子对象。为什么不呢?,c++,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 为什么不允许零大小的成员

这是今天零尺寸对象和子对象系列中的第三个问题。 标准清楚地表明,成员子对象的大小不能为零,而基类子对象的大小可以为零

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
为什么不允许零大小的成员子对象,就像零大小的基类子对象一样

短暂性脑缺血发作

康拉德回答后编辑: 考虑下面的例子:

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的示例(见下面的答案),如果同一类中有多个相同类型的子对象,则不允许零大小的成员子对象。我错在哪里

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

因为同一类型的多个(子)对象可能具有相同的地址,这是标准所禁止的:

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);

这有点合乎逻辑:否则,这两个对象在所有意图和目的上都是相同的(因为对象的标识完全由其类型和内存地址决定),单独声明它们是没有意义的。

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

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

C兼容性 在本例中:

struct X{};
struct Y{};
struct Z {
   struct X x;
   struct Y y;
   int i;
};
根据C++03规则,
Z
将是一个POD,但如果
x
y
是零大小的子对象,则布局与C不兼容。C布局兼容性是PODs存在的原因之一。这个问题不会发生在基类上,因为C没有基类,而在C++03中有基类的类不是pod,所以所有的赌注都是无效的:)。访问者注意到C实际上不支持空结构。所以整个论点都是错误的。我只是假设是这样的——这似乎是一个无害的概括

此外,程序似乎假设了一些事情,比如
y
的地址大于
x
,诸如此类的事情——这是由5.10/2中指针上的关系运算符保证的。我真的不知道是否有令人信服的理由允许这样做,或者有多少程序在实践中使用它

依我看,这是所有这些中最有力的论点

不能很好地推广到数组 继续上面的示例,添加以下内容:

struct Z1 {
   struct X x[1];
   struct Y y[1];
   int i;
};
…人们可能期望
sizeof(Z1)=sizeof(Z)
,并且
x
y
也可以像普通数组一样工作(即,可以形成一个超过结束指针的指针,该指针与任何元素的地址都不同)。这些期望之一将被零大小的子对象打破

没有基本情况那么令人信服 从空基派生的主要原因之一是因为它是策略或接口类型类。这些代码通常是空的,要求它们占用空间会造成“抽象惩罚”,即使组织更好的代码更加臃肿。这是Stroustrup在C++中不需要的——他希望在最小运行时成本下适当的抽象。 另一方面,如果声明类型的成员,则不会继承其函数、typedef等;而且您不会得到从派生到基的特殊指针转换,因此可能没有理由使用零大小的成员而不是零大小的基

这里的一个反例类似于STL容器中的分配器策略类——您不一定希望容器派生自它,但您希望“保持它”而不占用开销

空基类案例涵盖了大多数用途 …如果担心空间开销,可以使用私有继承而不是声明成员。虽然没有那么直接,但你或多或少也能达到同样的效果。显然,如果您有很多空成员,并且希望占用零空间,那么这就不能很好地工作

这是另一个特例 有相当多的微妙的事情,不与此优化工作。例如,您不能
memcpy
将零大小的POD子对象的位放入
char
数组,然后返回,或在零大小的子对象之间返回。我见过有人用
memcpy
实现
operator=
(我不知道为什么…),这会破坏这类东西。假定为基类而不是成员破坏这些东西的问题不大。

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

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

因此,这样的优化将更加复杂,更加微妙,更可能以意想不到的方式中断

我不能马上举出任何零尺寸成员对象肯定会断裂的例子,但我不相信它们也不存在。我已经指出了一些限制