为什么C++11的POD“标准布局”定义是这样的?

53

我正在研究C++11中新的、放松的POD定义(第9.7节)。

标准布局类是指:

  • 没有非标准布局类(或此类类型数组)或引用类型的非静态数据成员,
  • 没有虚函数(10.3)和虚基类(10.1),
  • 所有非静态数据成员具有相同的访问控制级别(第11条款),
  • 没有非标准布局基类,
  • 在最派生类中没有非静态数据成员,而只有一个带有非静态数据成员的基类,或者没有带有非静态数据成员的基类,
  • 没有与第一个非静态数据成员相同类型的基类

我已经突出了让我感到惊讶的部分。

如果我们容忍具有不同访问控制级别的数据成员会出现什么问题?

如果第一个数据成员也是一个基类会出现什么问题?即:

struct Foo {};
struct Good : Foo {int x; Foo y;};
struct Bad  : Foo {Foo y; int x;};

我承认这是一种奇怪的结构,但为什么应该禁止Bad而不是Good

最后,如果有多个成员类具有数据成员会出现什么问题?


struct一直都有所有成员都是public的特性。C++11现在支持private了吗? - user195488
@Mu:是的,结构体的成员默认为公共的。相反地,类的成员默认为私有的。 - user195488
15
在C++中,结构体(struct)中是可以有private成员的,但在C语言中不行。默认情况下是public - Sven
1
@Code Monkey:在结构体中定义受保护和私有成员一直是合法的,唯一的区别只是默认值。 - Puppy
6个回答

29

根据后面的一些段落,您被允许将标准布局类对象的地址转换为其第一个成员的指针,然后再转回去。 这在C语言中也经常这样做:

struct A { int x; };
A a;

// "px" is guaranteed to point to a.x
int *px = (int*) &a;

// guaranteed to point to a
A *pa = (A*)px; 
为了使此操作生效,第一个成员和整个对象必须具有相同的地址(编译器不能调整int指针任何字节,因为它无法知道它是否是A的成员)。
最后,如果超过一个构成类具有数据成员,会出现什么问题?
在类中,成员按照声明顺序按递增地址分配。然而C++不规定跨类的数据成员分配顺序。如果派生类和基类都有数据成员,则标准有意不定义其地址顺序,从而使实现在排列内存时具有充分的灵活性。但是,为了使上述转换生效,您需要知道分配顺序中的“第一个”成员!
如果第一个数据成员也是基类,会出现什么问题?
如果基类与第一个数据成员具有相同类型,那么在内存中将基类放置在派生类对象之前的实现将需要在派生类对象数据成员之前在内存中有一个填充字节(基类大小为1),以避免基类和第一个数据成员具有相同的地址(在C ++中,两个相同类型的不同对象始终具有不同的地址)。但是这将再次使将派生类对象的地址转换为其第一个数据成员的类型变得不可能。

@spraff,规范允许标准布局类,而不仅仅是聚合类。只有函数而没有数据成员的基类可以通过特性或SFINAE用例出现。在这种情况下,破坏第一个数据成员之前没有填充的保证将是遗憾的,我认为。 - Johannes Schaub - litb
对于非数据承载的基类和作为成员的同一类,放宽具有不同指针的不同对象的要求怎么样?这会引起令人讨厌的副作用吗?(据我所知,在钻石问题下,重复的空基类已经因为没有填充保证而拥有两个“实例”共享一个指针值) - spraff
@spraff,我没有看到矛盾之处。你能举个例子吗? - Johannes Schaub - litb
1
@spraff你的例子与“没有与第一个非静态数据成员相同类型的基类”相矛盾,因此对于它来说,“Derived::d之前没有填充”的保证是不存在的。 - Johannes Schaub - litb
@litb,你对这种话题(以及整个C++)的细致理解,让我想引起你对这个问题,我怀疑它调用了标准布局规则的注意。我相信如果我能更好地读懂其中的含义,我就可以弄清楚我所问的是否合法。但不幸的是,我很困惑,所以如果你能给我一个明确的答案,我将非常感激并奖励你赏金。更多积分! :-) - HostileFork says dont trust SE
显示剩余8条评论

28

这基本上涉及与C++03和C的兼容性:

  • 相同的访问控制 - C++03实现可以使用访问控制修饰符作为重新排序类(组)成员的机会,例如为了更好地压缩。
  • 具有非静态数据成员的多个继承类 - C++03没有说明基类位于何处,或者是否省略在完整对象中存在的基类子对象中的填充。
  • 基类和相同类型的第一个成员 - 由于第二条规则,如果基类类型用于数据成员,则必须是空类。许多编译器确实实现了空基类优化,因此Andreas所说的子对象具有相同地址的情况是正确的。不过,我不确定标准布局类的什么特点意味着基类子对象具有与相同类型的第一个数据成员相同的地址是不好的,但当基类子对象具有与不同类型的第一个数据成员相同的地址时,这并不重要。[编辑:这是因为相同类型的不同对象具有不同的地址,即使它们是空子对象。感谢Johannes]

C++0x可能也可以定义这些东西为标准布局类型,这样它也将定义它们的布局方式,就像它为标准布局类型所做的那样。 Johannes的回答进一步探讨了这个问题,请看他关于标准布局类的一个不错特性的示例,这些问题会干扰它。

但是,如果这样做,那么一些实现将被迫更改它们排列类的方式以匹配新要求,这对于在C++0x之前和之后的编译器版本之间的结构兼容性来说是一种麻烦。它基本上破坏了C++ ABI。

我对标准布局的定义理解是,他们考虑可以放松哪些POD要求而不破坏现有实现。因此,我认为,未经检查,上述是一些C++03实现确实使用类的非POD性质来做与标准布局不兼容的事情的例子。


起初我接受了这个观点,但是越想越觉得编译器如何对类/结构体的成员进行排序/对齐似乎并不重要——只要基类和成员结构体在编译器布局时保持自身所属类的状态即可。如果我们允许异构访问控制和多个带成员的基类,那么任何曾经是POD的东西都不会因此而停止成为POD! - spraff
值得注意的是,C++11澄清了“相同访问控制”/“按访问控制说明符重新排序”的含义,只有当说明符不同时才会进行重新排序 - 以防万一您想包含多个冗余的相同说明符 - 而C++03允许在这些说明符周围进行重新排序。至于是否有任何编译器实际执行了后者,我不知道。 - underscore_d
@Steve Jessop "所以Andreas所说的子对象具有",我相信你指的是成员子对象。 - RaGa__M

9
如果我们容忍访问控制不同的数据成员,会出现什么问题?
目前的语言规定编译器不能重新排列相同访问控制下的成员,例如:
struct x
{
public:
    int x;
    int y;
private:
    int z;
};

这里需要先分配 x,然后才能分配 y,但是对于 z 相对于 x 和 y 的顺序没有限制。

struct y
{
public:
    int x;
public:
    int y;
};

新的用语表明,尽管有两个public,但y仍然是POD。这实际上是对规则的放宽。

4

1
如果 sizeof(Foo) 不为零,则 struct Bar:Foo{Foo f;}; 将由两个不同的 Foo 组成,位于两个不同的位置。这是一个很好的链接,但我看不出这两个 Foo 如何可以具有相同的地址。 - spraff
1
请注意,如果有第一个数据成员,则基类必须*具有零大小(根据第5点,如果它具有非静态数据成员,则任何基类都不能拥有它们,因此所有基类都具有零大小)。 - Jan Hudec
3
“@spraff: that doesn't follow. sizeof(Foo) is non-zero for any class Foo, but if the class is empty then even though it has non-zero size, when used as a subobject it can occupy no space.”这句话的意思是:无法推导出结论。“sizeof(Foo)”对于任何类“Foo”都是非零的,但如果该类为空,则即使它具有非零大小,当用作子对象时,它也可以不占用空间。 - Steve Jessop

2
从第5点来看,由于最派生类具有非静态数据成员(int),它不能具有具有非静态数据成员的基类。
我的理解是:“只有“基”类中的一个(即类本身或其继承的类之一)可以具有非静态数据成员”。

“只有一个'base'…” -- 是的,但是为什么 - spraff
3
从上述引用的论文来看,问题似乎在于标准没有对基类数据与派生类数据的分配位置做出任何限制,即布局顺序(基类数据和派生类数据)未被明确指定。因此,这将破坏POD类型的布局兼容性保证(如果我理解正确的话)。 - Nicolas Grebille

1

Good结构体也不是标准布局,因为Foo和Good都有非静态数据成员。

这样,Good结构体应该是:

struct Foo {int foo;};
struct Good : public Foo {Foo y;};

无法满足第六个要求。因此第六个要求是什么?


1
你的 Good 版本也没有满足第五条规则。一个具有非静态数据成员的类不能有一个同样具有非静态数据成员的基类。第五条规则指出,只能有一个非静态数据成员块。该块可以在类本身中或者在其一个基类中。第六条规则是针对第一个非静态数据成员是一个类的情况。如果它是空的,并且有一个相同类型的基类,则基对象的地址和第一个数据成员的地址可能相等,这是不允许的。 - Rob Kennedy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接