什么是聚合体和POD,以及它们如何/为什么是特殊的?
这个常见问题是关于聚合和PODs,包括以下材料:
- 什么是聚合 ?
- 什么是POD (普通旧数据)?
- 他们有什么关系?
- 他们如何以及为什么特别?
- C ++ 11有什么变化?
如何阅读:
这篇文章相当长。 如果你想了解聚合和POD(普通旧数据),请花点时间阅读。 如果你只是在聚合感兴趣,只读第一部分。 如果您只对POD感兴趣,那么您必须先阅读聚合的定义,含义和示例,然后您可以跳到POD,但我仍然build议阅读第一部分的全部内容。 聚合的概念对于定义POD是必不可less的。 如果您发现任何错误(甚至包括语法,文体学,格式,语法等轻微问题),请留下评论,我会编辑。
什么是聚合,为什么它们是特殊的
来自C ++标准的正式定义( C ++ 03 8.5.1§1 ) :
聚合是没有用户声明的构造函数(12.1),没有私有或受保护的非静态数据成员(第11节),没有基类(第10节),也没有虚函数(10.3)的数组或类)。
好的,我们来parsing一下这个定义。 首先,任何数组都是一个聚合。 一个类也可以是一个聚合如果…等待! 关于结构或工会什么都没有说,他们不可能是聚合? 是的他们可以。 在C ++中,术语class
是指所有的类,结构和联合。 所以,当且仅当它满足上述定义的标准时,类(或结构或联合)才是聚合。 这些标准意味着什么?
-
这并不意味着聚合类不能有构造函数,实际上它可以有一个默认的构造函数和/或一个复制构造函数,只要它们是由编译器隐式声明的,而不是由用户明确地声明的
-
没有私人或受保护的非静态数据成员 。 您可以拥有尽可能多的私有和受保护的成员函数(但不包括构造函数)以及任意数量的私有或受保护的静态数据成员和成员函数,并且不违反聚合类的规则
-
聚合类可以具有用户声明/用户定义的复制赋值运算符和/或析构函数
-
即使数组是非聚合类types的数组,也是聚合数据。
现在我们来看一些例子:
class NotAggregate1 { virtual void f() {} //remember? no virtual functions }; class NotAggregate2 { int x; //x is private by default and non-static }; class NotAggregate3 { public: NotAggregate3(int) {} //oops, user-defined constructor }; class Aggregate1 { public: NotAggregate1 member1; //ok, public member Aggregate1& operator=(Aggregate1 const & rhs) {/* */} //ok, copy-assignment private: void f() {} // ok, just a private function };
你明白了。 现在让我们看看聚合是如何特殊的。 与非聚合类不同,它们可以用大括号{}
进行初始化。 这个初始化语法通常是已知的数组,我们刚刚了解到这些是聚合。 那么,让我们从他们开始。
Type array_name[n] = {a 1 , a 2 , …, a m };
如果(m == n)
数组的第 i 个元素用i初始化
否则如果(m <n)
数组的前m个元素用1,2 ,…,a m初始化,其他n - m
元素在可能的情况下被初始化 (参见下面的解释)
否则如果(m> n)
编译器会发出一个错误
else (当n完全没有被指定,就像int a[] = {1, 2, 3};
)
假设数组(n)的大小等于m,所以int a[] = {1, 2, 3};
相当于int a[3] = {1, 2, 3};
当标量types的对象( bool
, int
, char
, double
,指针等)被初始化时,这意味着它被初始化为0
(对于bool
为false
,对于double
等为0)。 当一个带有用户声明的默认构造函数的类types对象被初始化时,它的默认构造函数被调用。 如果默认构造函数是隐式定义的,那么所有非静态成员都是recursion值初始化的。 这个定义是不准确的,有点不正确,但它应该给你基本的想法。 引用不能被初始化。 例如,如果类没有适当的默认构造函数,则非聚合类的值初始化可能会失败。
数组初始化的例子:
class A { public: A(int) {} //no default constructor }; class B { public: B() {} //default constructor available }; int main() { A a1[3] = {A(2), A(1), A(14)}; //OK n == m A a2[3] = {A(2)}; //ERROR A has no default constructor. Unable to value-initialize a2[1] and a2[2] B b1[3] = {B()}; //OK b1[1] and b1[2] are value initialized, in this case with the default-ctor int Array1[1000] = {0}; //All elements are initialized with 0; int Array2[1000] = {1}; //Attention: only the first element is 1, the rest are 0; bool Array3[1000] = {}; //the braces can be empty too. All elements initialized with false int Array4[1000]; //no initializer. This is different from an empty {} initializer in that //the elements in this case are not value-initialized, but have indeterminate values //(unless, of course, Array4 is a global array) int array[2] = {1, 2, 3, 4}; //ERROR, too many initializers }
现在让我们看看如何用花括号初始化聚合类。 几乎相同的方式。 我们将按照在类定义中出现的顺序来初始化非静态数据成员(它们都是按照定义公开的),而不是数组元素。 如果初始化器比成员less,其余的都是初始化的。 如果无法对其中一个未明确初始化的成员进行值初始化,则会出现编译时错误。 如果有更多的初始化程序比我们需要的更多,我们也会遇到编译时错误。
struct X { int i1; int i2; }; struct Y { char c; X x; int i[2]; float f; protected: static double d; private: void g(){} }; Y y = {'a', {10, 20}, {20, 30}};
在上面的例子中, yc
初始化为'a'
, yxi1
为10
, yxi2
为20
, yi[0]
为20
, yi[1]
为30
和yf
为初始值,即初始化为0.0
。 被保护的静态成员d
根本就没有被初始化,因为它是static
。
聚合工会是不同的,你可以初始化他们的第一个成员带大括号。 我认为,如果你在C ++中足够先进,甚至考虑使用工会(他们的使用可能是非常危险的,必须仔细考虑),你可以自己查找标准工会的规则:)。
现在我们知道聚合的特殊之处,让我们试着了解对类的限制; 那就是为什么他们在那里。 我们应该明白,使用花括号进行成员初始化意味着该类只不过是其成员的总和。 如果用户定义的构造函数存在,这意味着用户需要做一些额外的工作来初始化成员,因此大括号初始化将是不正确的。 如果存在虚函数,则意味着这个类的对象(在大多数实现中)有一个指向所谓的构造函数中设置的类的vtable的指针,所以,括号初始化是不够的。 你可以用类似的方式来计算其余的限制:)。
足够的关于总量。 现在我们可以定义一个更严格的types,就是POD
什么是POD,为什么它们是特殊的
来自C ++标准的正式定义( C ++ 03 9§4 ) :
POD-struct是一个没有非POD-struct,non-POD-union(或这种types的数组)或非引用的非静态数据成员的聚合类,也没有用户定义的复制赋值运算符,也没有用户定义的析构函数。 同样,POD-union是一个聚合联合,没有types非POD-struct,non-POD-union(或这种types的数组)或非引用的非静态数据成员,也没有用户定义的复制赋值运算符并没有用户定义的析构函数。 POD类是一个POD结构或POD结合的类。
哇,这个更难parsing,不是吗? :)让我们离开工会(与上面相同的理由),并以更清晰的方式重新表述:
如果聚合类没有用户定义的复制赋值运算符和析构函数,并且它的非静态成员都不是非POD类,非POD数组或非引用数据类,则称其为聚合类。
这个定义暗示了什么? (我提到POD代表Plain Old Data吗?)
- 所有的POD类都是集合,或者换句话说,如果一个类不是一个聚集,那肯定不是一个POD
- 就像结构一样,类也可以是POD,即使这两种情况的标准术语都是POD结构
- 就像聚合的情况一样,这个类有什么静态成员并不重要
例子:
struct POD { int x; char y; void f() {} //no harm if there's a function static std::vector<char> v; //static members do not matter }; struct AggregateButNotPOD1 { int x; ~AggregateButNotPOD1() {} //user-defined destructor }; struct AggregateButNotPOD2 { AggregateButNotPOD1 arrOfNonPod[3]; //array of non-POD class };
POD类,POD联合,标量types和这种types的数组统称为PODtypes。
POD在许多方面是特殊的。 我只提供一些例子。
-
POD类是最接近C结构的。 与他们不同的是,POD可以有成员函数和任意静态成员,但是这两者都不能改变对象的内存布局。 所以,如果你想编写一个或多或less的可移植的dynamic库,可以从C甚至.NET使用,你应该尝试使所有的导出函数,并返回只有PODtypes的参数。
-
非POD类types的对象的生命周期在构造函数完成时开始,在析构函数完成时结束。 对于POD类,当存储对象被占用时,生命周期开始,当释放或重用存储时结束。
-
对于PODtypes的对象,通过标准保证,当你将对象的内容存入char或unsigned char数组,然后将内容
memcpy
返回到你的对象中时,对象将保持其原始值。 请注意,对于非PODtypes的对象没有这样的保证。 此外,您可以安全地复制POD对象与memcpy
。 以下示例假设T是PODtypes:#define N sizeof(T) char buf[N]; T obj; // obj initialized to its original value memcpy(buf, &obj, N); // between these two calls to memcpy, // obj might be modified memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type // holds its original value
-
goto声明。 正如你所知道的,编译器应该发出一个错误,通过goto从某个variables尚未处于范围的地方跳转到已经处于范围的地方。 此限制仅适用于variables为非PODtypes的情况。 在下面的例子中,
f()
g()
是格式不正确的,而g()
是格式良好的。 请注意,微软的编译器对这个规则太自由了 – 它只是在这两种情况下发出警告。int f() { struct NonPOD {NonPOD() {}}; goto label; NonPOD x; label: return 0; } int g() { struct POD {int i; char c;}; goto label; POD x; label: return 0; }
-
保证在POD对象的开始处不会有填充。 换句话说,如果一个POD-class A的第一个成员是Ttypes,那么你可以安全地从
A*
到T*
reinterpret_cast
并获取指向第一个成员的指针,反之亦然。
名单继续下去…
结论
了解POD究竟是什么是很重要的,因为正如你所看到的,许多语言特征对他们来说行为不同。
C ++ 11有什么变化?
骨料
聚合的标准定义略有改变,但仍然几乎相同:
聚合是没有用户提供的构造函数(12.1)的数组或类(第9章),对于非静态数据成员(9.2)没有括号或等于初始值设定项,没有私有或受保护的非静态数据成员第11章),没有基类(第10章),也没有虚函数(10.3)。
好的,什么改变了?
-
以前,聚合可能没有用户声明的构造函数,但现在不能有用户提供的构造函数。 有区别吗? 是的,因为现在你可以声明构造函数并默认它们:
struct Aggregate { Aggregate() = default; // asks the compiler to generate the default implementation };
这仍然是一个聚合,因为在第一个声明默认的构造函数(或任何特殊的成员函数)不是用户提供的。
-
现在一个聚合不能有非静态数据成员的任何括号或等于初始值设定项。 这是什么意思? 那么,这只是因为有了这个新的标准,我们可以像这样直接在类中初始化成员:
struct NotAggregate { int x = 5; // valid in C++11 std::vector<int> s{1,2,3}; // also valid };
使用这个特性使得这个类不再是一个聚合,因为它基本上等同于提供你自己的默认构造函数。
所以,什么是一个聚合没有太大的改变。 它仍然是一个基本的想法,适应新的特点。
POD呢?
POD经历了很多变化。 在这个新的标准中,许多关于POD的规则被放宽了,定义在标准中提供的方式也发生了根本性的变化。
POD的想法是捕捉基本上两个不同的属性:
- 它支持静态初始化,
- 用C ++编译一个POD,就可以得到与C编译的结构相同的内存布局。
正因为如此,这个定义被分成两个不同的概念: 普通类和标准布局类,因为这些比POD更有用。 现在这个标准很less使用POD这个术语,更喜欢更具体的平凡和标准的布局概念。
新的定义基本上说,POD是一个既平凡又具有标准布局的类,并且该属性必须recursion地保存所有非静态数据成员:
POD结构是一个非联合类,它既是一个普通的类也是一个标准布局类,并且没有types非POD结构,非POD联合(或这种types的数组)的非静态数据成员。 同样,POD联合是既是平凡类又是标准布局类的联合,并且没有types非POD结构,非POD联合(或这种types的数组)的非静态数据成员。 POD类是一个POD结构或POD结合的类。
我们分别详细介绍这两个属性的每一个。
琐碎的课程
Trivial是上面提到的第一个属性:普通类支持静态初始化。 如果一个类是可复制的(一个普通的类的超集),那么可以将它的表示复制到像memcpy
这样的东西上,并期望结果是相同的。
该标准定义了一个微不足道的类如下:
一个可复制的类是一个类:
– 没有非平凡的拷贝构造函数(12.8),
– 没有不小的动作构造函数(12.8),
– 没有非平凡的复制分配操作符(13.5.3,12.8),
– 没有不平凡的移动赋值操作符(13.5.3,12.8)和
– 有一个微不足道的析构函数(12.4)。
一个普通的类是一个有一个微不足道的默认构造函数(12.1),是可以复制的。
[ 注意:特别是一个可复制或平凡的类没有虚函数或虚基类。 – 注意 ]
那么,那些微不足道和不平凡的事情是什么呢?
类X的复制/移动构造函数是微不足道的,如果它不是用户提供和如果
– X类没有虚函数(10.3),也没有虚函数类(10.1)
– select复制/移动每个直接基类子对象的构造函数是微不足道的
– 对于types(或其数组)的X的每个非静态数据成员,select复制/移动该成员的构造函数是微不足道的;
否则复制/移动构造函数是不平凡的。
基本上这意味着如果不是用户提供的,复制或移动构造函数是微不足道的,类中没有任何虚拟内容,并且该属性recursion地为类的所有成员和基类保存。
简单复制/移动赋值运算符的定义非常相似,只需将“构造函数”replace为“赋值运算符”即可。
一个微不足道的析构函数也有类似的定义,增加了约束,它不能是虚拟的。
还有另外一个类似的规则存在于简单的默认构造函数中,另外,如果类的非静态数据成员带有括号或等于初始值设定项,那么默认的构造函数就不是微不足道的。
这里有一些例子来清除一切:
// empty classes are trivial struct Trivial1 {}; // all special members are implicit struct Trivial2 { int x; }; struct Trivial3 : Trivial2 { // base class is trivial Trivial3() = default; // not a user-provided ctor int y; }; struct Trivial4 { public: int a; private: // no restrictions on access modifiers int b; }; struct Trivial5 { Trivial1 a; Trivial2 b; Trivial3 c; Trivial4 d; }; struct Trivial6 { Trivial2 a[23]; }; struct Trivial7 { Trivial6 c; void f(); // it's okay to have non-virtual functions }; struct Trivial8 { int x; static NonTrivial1 y; // no restrictions on static members }; struct Trivial9 { Trivial9() = default; // not user-provided // a regular constructor is okay because we still have default ctor Trivial9(int x) : x(x) {}; int x; }; struct NonTrivial1 : Trivial3 { virtual void f(); // virtual members make non-trivial ctors }; struct NonTrivial2 { NonTrivial2() : z(42) {} // user-provided ctor int z; }; struct NonTrivial3 { NonTrivial3(); // user-provided ctor int w; }; NonTrivial3::NonTrivial3() = default; // defaulted but not on first declaration // still counts as user-provided struct NonTrivial5 { virtual ~NonTrivial5(); // virtual destructors are not trivial };
标准布局
标准布局是第二个属性。 标准提到这些对于与其他语言进行通信是有用的,这是因为标准布局类具有相同的等效C结构或联合的内存布局。
这是另一个必须recursion的成员和所有基类的属性。 和往常一样,不允许虚拟函数或虚拟基类。 这将使布局不符合C.
这里宽松的规则是,标准布局类必须具有所有具有相同访问控制的非静态数据成员。 以前这些都必须是公开的 ,但是现在你可以把它们变成私有的或者保护的,只要它们全部是私有的或者全部受保护的。
当使用inheritance时,整个inheritance树中只有一个类可以有非静态数据成员,第一个非静态数据成员不能是基类types(这可能会破坏别名规则),否则,它不是标准的,布局类。
这就是标准文本中的定义:
标准布局类是一个类:
– 没有types非标准布局类(或这种types的数组)的非静态数据成员或引用,
– 没有虚函数(10.3),没有虚基类(10.1),
– 对所有非静态数据成员具有相同的访问控制(第11章)
– 没有非标准的布局基础类,
– 在大多数派生类中最多有一个非静态数据成员,最多有一个基类具有非静态数据成员,或没有包含非静态数据成员的基类,以及
– 没有与第一个非静态数据成员相同types的基类。
标准布局结构是使用类键结构或类键类定义的标准布局类。
标准布局联合是用类联合键定义的标准布局类。
[ 注意:标准布局类对于用其他编程语言编写的代码进行通信非常有用。 它们的布局在9.2中指定。 – 注意 ]
我们来看几个例子。
// empty classes have standard-layout struct StandardLayout1 {}; struct StandardLayout2 { int x; }; struct StandardLayout3 { private: // both are private, so it's ok int x; int y; }; struct StandardLayout4 : StandardLayout1 { int x; int y; void f(); // perfectly fine to have non-virtual functions }; struct StandardLayout5 : StandardLayout1 { int x; StandardLayout1 y; // can have members of base type if they're not the first }; struct StandardLayout6 : StandardLayout1, StandardLayout5 { // can use multiple inheritance as long only // one class in the hierarchy has non-static data members }; struct StandardLayout7 { int x; int y; StandardLayout7(int x, int y) : x(x), y(y) {} // user-provided ctors are ok }; struct StandardLayout8 { public: StandardLayout8(int x) : x(x) {} // user-provided ctors are ok // ok to have non-static data members and other members with different access private: int x; }; struct StandardLayout9 { int x; static NonStandardLayout1 y; // no restrictions on static members }; struct NonStandardLayout1 { virtual f(); // cannot have virtual functions }; struct NonStandardLayout2 { NonStandardLayout1 X; // has non-standard-layout member }; struct NonStandardLayout3 : StandardLayout1 { StandardLayout1 x; // first member cannot be of the same type as base }; struct NonStandardLayout4 : StandardLayout3 { int z; // more than one class has non-static data members }; struct NonStandardLayout5 : NonStandardLayout3 {}; // has a non-standard-layout base class
结论
有了这些新的规则,更多的types可以成为POD。 即使一个types不是POD,我们也可以分别利用一些POD属性(如果它只是一个普通的或标准的布局)。
标准库具有在头文件<type_traits>
testing这些属性的特性:
template <typename T> struct std::is_pod; template <typename T> struct std::is_trivial; template <typename T> struct std::is_trivially_copyable; template <typename T> struct std::is_standard_layout;
什么改变了C ++ 14
我们可以参考Draft C ++ 14标准以供参考。
骨料
这在第8.5.1
节中有详细说明,它给了我们下面的定义:
聚合是没有用户提供的构造函数(12.1),没有私有或受保护的非静态数据成员(第11章),没有基类(第10章),也没有虚函数(10.3)的数组或类(第9章) )。
现在唯一的变化是添加类内成员初始值设定项不会使类成为非聚合类。 因此, C ++ 11中的以下示例为具有成员即时初始化程序的类进行聚合初始化 :
struct A { int a = 3; int b = 3; };
在C ++ 11中不是一个聚合,而是在C ++ 14中。 N3605:成员初始值设定项和汇总项包含以下摘要:
Bjarne Stroustrup和Richard Smith提出了关于聚合初始化和成员初始化不能一起工作的问题。 本文提出通过采用史密斯提出的措辞来解决这个问题,该措辞消除了聚合不能具有成员初始化者的限制。
POD保持不变
POD( 普通旧数据 )结构的定义在第9
节中讲述:
POD结构体110是一个非联合类,它既是一个普通的类又是一个标准布局类,并且没有types非POD结构,非POD联合(或这种types的数组)的非静态数据成员。 同样,POD联合是既是一个普通类又是一个标准布局类的联合,并且没有types非POD结构,非POD联合(或这种types的数组)的非静态数据成员。 POD类是一个POD结构或POD结合的类。
这与C ++ 11相同。
请你详细说明下列规则:
我会尽力:
a)标准布局类必须具有所有具有相同访问控制的非静态数据成员
很简单:所有非静态数据成员都必须是public
, private
或protected
。 你不能有一些public
和一些private
。
他们的推理是根据“标准布局”和“非标准布局”之间的区别来推理的。 也就是说,让编译器自由select如何把东西放入内存中。 这不仅仅是关于vtable指针。
当他们在98年对C ++进行标准化时,他们不得不基本预测人们将如何实现它。 虽然他们在C ++的各种风格上有相当多的实现经验,但他们对事物并不确定。 所以他们决定保持谨慎:给编译器尽可能多的自由。
这就是为什么在C ++ 98中定义POD非常严格的原因。 它为C ++编译器提供了对大多数类的成员布局的很大的自由度。 基本上,PODtypes意图是特殊情况,你特别写了一些原因。
当C ++ 11正在开发时,他们有更多的编译器经验。 而他们意识到… C ++编译器的编写者真的很懒惰。 他们拥有所有的自由,但他们没有做任何事情。
标准布局的规则或多或less被编纂成为惯例:大多数编译器并不需要做太多的改变来实现它们(除了相应types特性的某些东西之外)。
现在,当涉及public
/ private
,事情是不同的。 重新sorting哪些成员是public
private
的自由对编译器来说可能很重要,特别是在debugging构build时。 而且由于标准布局的要点是与其他语言兼容,所以不能在debugging版本和发布版本中使布局不同。
然后有一个事实,它并没有真正伤害用户。 如果你正在制作一个封装类,好处是所有的数据成员都是private
的。 您通常不公开数据成员完全封装types。 所以对于那些想要这样做的用户来说,这只会是一个问题,谁想要这个部门。
所以这不是什么大的损失。
b)整个inheritance树中只有一个类可以有非静态数据成员,
之所以这样做,是因为他们为什么要重新规范标准的布局:通常的做法。
当有两个实际存储事物的inheritance树的成员时, 没有任何惯例。 有些把基类放在派生之前,另外一些则是这样做的。 如果他们来自两个基本类别,您如何订购成员? 等等。 编译器在这些问题上有很大的分歧。
另外,归功于零/一个/无穷大的规则,一旦你说你可以有两个类的成员,你可以说你想要的多。 这需要添加大量的布局规则来处理这个问题。 你必须说出多重inheritance是如何工作的,哪些类将数据放在其他类之前,等等。这是很多规则,对于很less的材料收益。
你不能做任何没有虚函数和默认构造器标准布局的东西。
并且第一个非静态数据成员不能是基类types(这可能会破坏别名规则)。
我真的不能说这个。 我没有足够的教训足够的C + +的别名的规则,真正理解它。 但是这与基本成员将与基类本身共享相同地址的事实有关。 那是:
struct Base {}; struct Derived : Base { Base b; }; Derived d; static_cast<Base*>(&d) == &d.b;
这可能是针对C ++的别名规则。 某种程度上来说。
但是,请考虑一下:能够做到这一点的能力究竟有多大用处? 由于只有一个类可以有非静态数据成员,因此Derived
必须是该类(因为它具有一个Base
成员)。 所以Base
必须是空的(数据)。 如果Base
是空的, 还有一个基类…为什么有一个数据成员呢?
由于Base
是空的,它没有状态。 所以任何非静态的成员函数都会根据它们的参数来做它们所做的事情,而不是它们的this
指针。
再一次:没有什么大的损失。