这个FAQ是关于聚合体和POD的,并涵盖以下内容:
- 什么是聚合体?
- 什么是POD(Plain Old Data)?
- 最近,什么是平凡或可平凡复制的类型?
- 它们之间有什么关系?
- 它们为什么特殊?
- C++11有什么变化?
这个FAQ是关于聚合体和POD的,并涵盖以下内容:
本文较长。如果您想了解聚合和POD(Plain Old Data),请花时间仔细阅读。如果您只对聚合感兴趣,请只阅读第一部分。如果您只对POD感兴趣,则必须先阅读聚合的定义、含义和示例,然后才可以跳转到POD,但我仍建议您完整阅读第一部分。聚合的概念对于定义POD至关重要。如果您发现任何错误(包括语法、格式、排版、语法等小错误),请留下评论,我会进行编辑。
本答案适用于C++03。其他C++标准请参见:
C++标准中的正式定义(C++03 8.5.1 §1):
聚合是一个数组或一个类(第9条),它没有用户声明的构造函数(12.1),没有私有或受保护的非静态数据成员(第11条),没有基类(第10条)和没有虚函数(第10.3条)。
所以,好吧,让我们解析这个定义。首先,任何数组都是聚合。如果…等等!没有提到结构体或联合体,它们不能是聚合吗?可以的。在C++中,术语“class”指的是所有类、结构体和联合体。因此,当且仅当一个类(或结构体、联合体)满足上述定义的条件时,它才是一个聚合。这些标准意味着什么?
这并不意味着聚合类不能有构造函数,实际上它可以有默认构造函数和/或复制构造函数,只要它们是由编译器隐式声明的,而不是由用户显式声明的
没有私有或受保护的非静态数据成员。您可以拥有尽可能多的私有和受保护的成员函数(但不是构造函数),以及尽可能多的私有或受保护的静态数据成员和成员函数,而不违反聚合类的规则
聚合类可以有用户声明/定义的复制赋值运算符和/或析构函数
即使它是非聚合类类型的数组,它也是一个聚合。
现在让我们看一些例子:
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] = {a1, a2, …, am};
如果(m == n)
则数组的第i个元素将被初始化为ai。
否则,如果(m < n)
数组的前m个元素将被初始化为a1, a2, …, am,而其他的n - m
个元素将会尽可能地进行值初始化(下面会解释这个术语)。
否则,如果(m > n)
编译器将会报错。
否则 (当n没有像int a[] = {1, 2, 3};
这样明确指定时)
则数组的大小(n)被假定为m,因此int a[] = {1, 2, 3};
等同于int a[3] = {1, 2, 3};
当标量类型的对象(bool
、int
、char
、double
、指针等)进行值初始化时,意味着它们会被初始化为该类型的0
(对于bool
是false
,对于double
是0.0
等)。当具有用户声明的默认构造函数的类类型对象进行值初始化时,其默认构造函数将被调用。如果默认构造函数是隐式定义的,则所有非静态成员都将递归地进行值初始化。这个定义不太精确,有点不正确,但应该能给你一个基本的概念。引用不能进行值初始化。对于非聚合类,值初始化可能会失败,例如,如果类没有适当的默认构造函数。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
}
现在让我们看看如何使用大括号初始化聚合类。这个过程和数组元素的初始化方式非常相似。我们会按照它们在类定义中出现的顺序初始化非静态数据成员(根据定义,它们都是公共的)。如果初始值少于成员数,其余成员将被值初始化。如果无法对未明确初始化的成员进行值初始化,则会在编译时出现错误。如果有多余的初始值,则同样会在编译时出现错误。
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}};
y.c
被初始化为'a'
,y.x.i1
被初始化为10
,y.x.i2
被初始化为20
,y.i[0]
被初始化为20
,y.i[1]
被初始化为30
,而y.f
则被值初始化,也就是用0.0
进行初始化。受保护的静态成员d
没有被初始化,因为它是static
类型。例子:
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类、POD联合体、标量类型以及这些类型的数组。
POD类型在很多方面都是特殊的。以下是一些例子:
POD-classes are the closest to C structs. Unlike them, PODs can have member functions and arbitrary static members, but neither of these two change the memory layout of the object. So if you want to write a more or less portable dynamic library that can be used from C and even .NET, you should try to make all your exported functions take and return only parameters of POD-types.
The lifetime of objects of non-POD class type begins when the constructor has finished and ends when the destructor has finished. For POD classes, the lifetime begins when storage for the object is occupied and finishes when that storage is released or reused.
For objects of POD types it is guaranteed by the standard that when you memcpy
the contents of your object into an array of char or unsigned char, and then memcpy
the contents back into your object, the object will hold its original value. Do note that there is no such guarantee for objects of non-POD types. Also, you can safely copy POD objects with memcpy
. The following example assumes T is a POD-type:
#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 statement. As you may know, it is illegal (the compiler should issue an error) to make a jump via goto from a point where some variable was not yet in scope to a point where it is already in scope. This restriction applies only if the variable is of non-POD type. In the following example f()
is ill-formed whereas g()
is well-formed. Note that Microsoft's compiler is too liberal with this rule—it just issues a warning in both cases.
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;
}
It is guaranteed that there will be no padding in the beginning of a POD object. In other words, if a POD-class A's first member is of type T, you can safely reinterpret_cast
from A*
to T*
and get the pointer to the first member and vice versa.
清单还有很多...
理解什么是POD很重要,因为像你看到的许多语言特性对它们的行为都不同。
private:
): struct A { int const a; };
,则A()
是良好定义的,即使'A'的默认构造函数定义是有问题的。 - Johannes Schaub - litb标准定义的聚合体略有改变,但基本上仍然是一样的:
聚合体是一个数组或类(第9条),没有用户提供的构造函数(12.1),非静态数据成员没有花括号或等号初始化符(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 的概念基本上捕获了两个不同的属性:
由于这个原因,这个定义被分成了两个不同的概念:平凡的类和标准布局类,因为它们比 POD 更实用。现在,标准很少使用术语 POD,而更喜欢更具体的平凡的和标准布局概念。
新的定义基本上说,POD 是一个既平凡又具有标准布局的类,并且对于所有非静态数据成员,这个属性必须递归地保持:
一个 POD 结构是一个非联合类,它既是一个平凡的类又是一个标准布局的类, 并且没有类型为非 POD 结构体、非 POD 联合体(或这些类型的数组)的非静态数据成员。 类似地,一个 POD 联合体是一个既平凡又具有标准布局的联合体,并且没有类型为非 POD 结构体、 非 POD 联合体(或这些类型的数组)的非静态数据成员。一个 POD 类是一个既是 POD 结构也是 POD 联合体的类。
让我们分别详细了解这两个属性。
平凡的是上面提到的第一个属性:平凡的类支持静态初始化。如果一个类是平凡可复制的(平凡类的超集),则可以使用诸如 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),
— 选中用于复制/移动每个直接基类子对象的构造函数是平凡的,以及
— 对于X的每个非静态数据成员(或其数组)的类类型,选中用于复制/移动该成员的构造函数是平凡的;
否则,复制/移动构造函数是非平凡的。
基本上,这意味着如果复制或移动构造函数不是用户提供的,类中没有虚拟内容,并递归地对类的所有成员和基类都具有此属性,则复制或移动构造函数是平凡的。
平凡复制/移动赋值运算符的定义非常相似,只需将“构造函数”替换为“赋值运算符”即可。
平凡析构函数也有一个类似的定义,增加的限制是它不能是虚拟的。
对于平凡默认构造函数,还存在另一个类似的规则,其中添加了一个默认构造函数是非平凡的条件,即类具有带有大括号或等号初始化程序的非静态数据成员。
以下是一些示例,以澄清所有内容:
// 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结构或联合相同的内存布局。
这是另一个必须递归地应用于成员和所有基类的属性。通常情况下,不允许使用虚拟函数或虚拟基类。否则,布局将与C不兼容。
这里的松散规则是,标准布局类必须具有具有相同访问控制的所有非静态数据成员。以前这些数据成员必须全部是public,但现在你可以使它们是all private或all protected,只要它们全部是private或protected即可。
在继承时,整个继承树中只有一个类可以具有非静态数据成员,并且第一个非静态数据成员不能是基类类型(这可能会破坏别名规则),否则它就不是标准布局类。
标准文本中的定义如下:
标准布局类是指:
— 没有非标准布局类(或这种类型的数组)或引用的非静态数据成员,
— 没有虚函数(10.3)和没有虚基类(10.1),
— 所有非静态数据成员均具有相同的访问控制(Clause 11),
— 没有非标准布局的基类,
— 在最派生类中没有非静态数据成员,并且最多有一个具有非静态数据成员的基类,或者没有具有非静态数据成员的基类,
— 没有与第一个非静态数据成员相同类型的基类。
标准布局结构体是使用class-key struct或class-key class定义的标准布局类。
标准布局联合体是使用class-key union定义的标准布局类。
[注意:标准布局类对于与其他编程语言编写的代码通信非常有用。它们的布局在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
结论
有了这些新规则,现在可以将更多类型定义为POD。即使一个类型不是POD,我们也可以单独利用其中一些POD属性(如果它只是平凡的或标准布局的其中之一)。
标准库在头文件
<type_traits>
中提供了测试这些属性的特性:
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标准草案。
在第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: Member initializers and aggregates中有详细说明:
Bjarne Stroustrup和Richard Smith提出了关于聚合初始化和成员初始化不能同时使用的问题。本文提议通过采用Smith提出的措辞来解决这个问题,即取消限制,允许聚合体具有成员初始化。
POD (plain old data) 结构的定义包含在第9
节的Classes中,该节规定:
一个POD结构体110是指既是平凡类又是标准布局类,并且没有类型为非POD结构体、非POD联合体(或此类类型的数组)的非静态数据成员。同样地,POD联合体是指既是平凡类又是标准布局类,并且没有类型为非POD结构体、非POD联合体(或此类类型的数组)的非静态数据成员。POD类是指既是POD结构体也是POD联合体的类。
这与C++11的措辞相同。
如评论中所指出的,pod依赖于standard-layout的定义,而此定义在C++14中已有更改,但这是通过缺陷报告在C++14之后应用的。
有三个DR:
因此,standard-layout从C++14之前的以下内容:
标准布局类是指:
- (7.1) 没有类型为非标准布局类(或此类类型的数组)或引用的非静态数据成员,
- (7.2) 没有虚函数([class.virtual])和虚基类([class.mi]),
- (7.3) 对于所有非静态数据成员,具有相同的访问控制(第[class.access]条款),
- (7.4) 没有非标准布局基类,
- (7.5) 在最派生类中没有非静态数据成员,在最多一个具有非静态数据成员的基类中有一个非静态数据成员,或者没有具有非静态数据成员的基类,
- (7.6) 没有与第一个非静态数据成员类型相同的基类。109
变为C++14中的以下内容:
如果一个类S满足以下条件,则它
在此处下载C++17国际标准最终草案。
聚合体
C++17扩展和增强了聚合体和聚合体初始化。标准库现在也包括一个std::is_aggregate
类型特征类。这是来自第11.6.1.1和11.6.1.2节的正式定义(省略了内部引用):
聚合体是一个数组或一个类,具有以下特点:
— 无用户提供、显式或继承构造函数,
— 没有私有或受保护的非静态数据成员,
— 没有虚函数,以及
— 没有虚、私有或受保护的基类。
[注意:聚合初始化不允许访问受保护和私有基类的成员或构造函数。—end note]
聚合体的元素为:
— 对于数组,按递增下标顺序的数组元素,或者
— 对于类,按声明顺序的直接基类,后跟不属于匿名联合的直接非静态数据成员,按声明顺序排列。
有什么变化?
struct B1 // not a aggregate
{
int i1;
B1(int a) : i1(a) { }
};
struct B2
{
int i2;
B2() = default;
};
struct M // not an aggregate
{
int m;
M(int a) : m(a) { }
};
struct C : B1, B2
{
int j;
M m;
C() = default;
};
C c { { 1 }, { 2 }, 3, { 4 } };
cout
<< "is C aggregate?: " << (std::is_aggregate<C>::value ? 'Y' : 'N')
<< " i1: " << c.i1 << " i2: " << c.i2
<< " j: " << c.j << " m.m: " << c.m.m << endl;
//stdout: is C aggregate?: Y, i1=1 i2=2 j=3 m.m=4
struct D // not an aggregate
{
int i = 0;
D() = default;
explicit D(D const&) = default;
};
struct B1
{
int i1;
B1() : i1(0) { }
};
struct C : B1 // not an aggregate
{
using B1::B1;
};
琐碎类
C++17重新制定了琐碎类的定义,以解决C++14中未解决的几个缺陷。这些更改是技术性的。以下是12.0.6的新定义(省略内部引用):
一个可平凡复制的类是一个类:
- 每个复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符要么被删除,要么是平凡的,
- 至少有一个未删除的复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符,
- 具有平凡的、未删除的析构函数。
一个平凡类是一个既可平凡复制又至少有一个默认构造函数的类,所有默认构造函数都是平凡的或已删除的,其中至少有一个未删除。[注意:特别地,平凡可复制或平凡类不具有虚函数或虚基类。-注]
更改:
std::memcpy
进行合法的复制/移动。这是一个语义上的矛盾,因为通过将所有构造函数/赋值运算符定义为已删除,该类的创建者明确地表达了该类不能被复制/移动,但该类仍符合平凡可复制类的定义。因此,在C++17中,我们有了一个新的条款,规定平凡可复制的类必须至少具有一个平凡的非删除复制/移动构造函数/赋值运算符(尽管不一定是公开可访问的)。请参见N4148, DR1734标准布局类
标准布局的定义也进行了修订,以解决缺陷报告。同样,这些更改在技术上具有意义。以下是标准中的文本(12.0.7)。与之前一样,省略了内部引用:
A class S is a standard-layout class if it:
— has no non-static data members of type non-standard-layout class (or array of such types) or reference,
— has no virtual functions and no virtual base classes,
— has the same access control for all non-static data members,
— has no non-standard-layout base classes,
— has at most one base class subobject of any given type,
— has all non-static data members and bit-fields in the class and its base classes first declared in the same class, and
— has no element of the set M(S) of types (defined below) as a base class.108
M(X) is defined as follows:
— If X is a non-union class type with no (possibly inherited) non-static data members, the set M(X) is empty.
— If X is a non-union class type whose first non-static data member has type X0 (where said member may be an anonymous union), the set M(X) consists of X0 and the elements of M(X0).
— If X is a union type, the set M(X) is the union of all M(Ui) and the set containing all Ui, where each Ui is the type of the ith non-static data member of X.
— If X is an array type with element type Xe, the set M(X) consists of Xe and the elements of M(Xe).
— If X is a non-class, non-array type, the set M(X) is empty.
[ Note: M(X) is the set of the types of all non-base-class subobjects that are guaranteed in a standard-layout class to be at a zero offset in X. —end note ]
[ Example:
—end example ]
struct B { int i; }; // standard-layout class struct C : B { }; // standard-layout class struct D : C { }; // standard-layout class struct E : D { char : 4; }; // not a standard-layout class struct Q {}; struct S : Q { }; struct T : Q { }; struct U : S, T { }; // not a standard-layout class
108) This ensures that two subobjects that have the same class type and that belong to the same most derived object are not allocated at the same address.
变更:
注意: C++标准委员会打算根据缺陷报告对上述更改适用于C++14,尽管新语言不在已发布的C++14标准中。它在C++17标准中。
C++11中的POD基本上被拆分为两个不同的轴线:平凡性和布局。平凡性关于一个对象的概念值与其存储内部数据位之间的关系。布局则是关于一个对象的子对象的布局。只有类类型具有布局,而所有类型都具有平凡性关系。
因此,这里是平凡性轴线所关注的内容:
非平凡可复制:这些类型的对象的值可能不仅仅是直接存储在对象中的二进制数据。
例如,unique_ptr<T>
存储一个 T*
;这是该对象中二进制数据的总和。但这并不是 unique_ptr<T>
的全部价值。 unique_ptr<T>
存储 nullptr
或由 unique_ptr<T>
实例管理生命周期的指向对象的指针。 这个管理是 unique_ptr<T>
值的一部分。 而且,该值不是对象的二进制数据的一部分; 它是由该对象的各种成员函数创建的。
例如,将 nullptr
分配给 unique_ptr<T>
不仅仅是改变对象中存储的位。 这样的分配必须销毁由 unique_ptr
管理的任何对象。 未经过其成员函数处理就操纵 unique_ptr
的内部存储会损坏这个机制,而更改其内部的 T*
而不破坏它当前管理的对象,将违反对象具有的概念值。
平凡可复制:这些对象的值恰好且只是其二进制存储内容。 这就是允许复制该二进制存储等同于复制对象本身的原因。
定义平凡复制能力(平凡析构函数、平凡/删除复制/移动构造函数/赋值)的特定规则是使类型成为仅二进制值的必要条件。对象的析构函数可以参与定义对象的“值”,例如 unique_ptr
。 如果该析构函数是平凡的,则不会参与定义对象的值。
专用的复制/移动操作也可以参与对象的值。 unique_ptr
的移动构造函数通过将源空置来修改移动操作的源。 这就确保了 unique_ptr
的值是唯一的。 平凡的复制/移动操作意味着没有进行此类对象值骗局,因此对象的值只能是它存储的二进制数据。
平凡:此对象被认为对其存储的任何位都具有功能值。平凡可复制定义对象的数据存储的含义只是该数据。但是这种类型仍然可以控制如何将数据放在那里(在某种程度上)。这种类型可以具有默认成员初始化程序和/或确保特定成员始终具有特定值的默认构造函数。 因此,对象的概念值可以限制为它可以存储的二进制数据的子集。
对具有平凡默认构造函数的类型执行默认初始化将使该对象具有完全未初始化的值。因此,具有平凡默认构造函数的类型在其数据存储中具有任何二进制数据都是逻辑上有效的。
struct Base {};
struct Derived : Base { Base b; };
Derived d;
static_cast<Base*>(&d) == &d.b;
这可能违反了C++的别名规则。在某种程度上。
然而,请考虑一下:拥有这种能力到底有多有用呢?由于只有一个类可以拥有非静态数据成员,那么Derived
必须是那个类(因为它有一个Base
作为成员)。所以Base
必须是空的(没有数据)。如果Base
为空,以及一个基类...为什么还要有它的数据成员呢?
由于Base
是空的,它没有状态。因此,任何非静态成员函数都将根据其参数而不是它们的this
指针执行它们的操作。
所以,再次强调:没有什么大的损失。
static_cast<Base*>(&d)
和&d.b
是相同的Base*
类型,但它们指向不同的东西,因此违反了别名规则。请纠正我。 - Andriy TylychkoDerived
必须是那个类呢? - Andriy TylychkoDerived
的第一个成员变为它的基类,它必须具备两个条件:一个基类和一个成员。而且由于在继承体系中只有一个类可以拥有成员(同时还保持标准布局),这意味着它的基类不能有成员。 - Nicol Bolas随着每个标准的推出,聚合体的含义和用法也在不断变化。未来将会有几个关键性的变化。
在C++17中,该类型仍然是一个聚合体:
struct X {
X() = delete;
};
X{}
仍然能编译通过,因为这是聚合初始化而不是构造函数调用。参见:何时私有构造函数不是私有构造函数?
在 C++20 中,限制将会从要求:
变为没有用户提供的、显式的或继承的构造函数
这已被采纳到 C++20 工作草案 中。在 C++20 中,既不是这里的没有用户声明的或继承的构造函数
X
,也不是链接问题中的 C
,都不会是聚合体。class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};
B
不是一个聚合体,因此B{}
执行值初始化,调用B::B()
,进而调用A::A()
,在这个点上它是可访问的。这是符合规范的。B
成为了一个聚合体,使得B{}
成为聚合初始化。这需要从{}
中复制列表初始化一个A
,但是从B
外部的上下文中,它是不可访问的。在C++17中,这是不符合规范的(尽管auto x = B();
是可以的)。B
再次不是一个聚合体(不是因为基类,而是由于用户声明的默认构造函数 - 即使它是默认的)。所以我们又回到了通过B
的构造函数,这段代码变得符合规范了。
一个常见的问题是想要在聚合类型中使用emplace()
风格的构造函数:
struct X { int a, b; };
std::vector<X> xs;
xs.emplace_back(1, 2); // error
这段代码无法工作,因为emplace
将尝试有效地执行初始化X(1, 2)
,但这是无效的。典型的解决方案是向X
添加构造函数,但是使用这个提案(目前正在通过核心层面),聚合体将有效地具有合成构造函数,做正确的事情并且像常规构造函数一样工作。在C++20中,上述代码将按原样编译。
在C++17中,以下代码无法编译:
template <typename T>
struct Point {
T x, y;
};
Point p{1, 2}; // error
用户需要为所有聚合模板编写自己的扣除指南:
template <typename T> Point(T, T) -> Point<T>;
但是,由于这在某种意义上是“显而易见的事情”,基本上只是样板文件,因此语言会为您完成这项工作。在C++20中,此示例将编译(无需用户提供的推断指南)。