什么是聚合和普通类型/POD,以及它们如何/为何特殊?
此常见问题解答是关于聚合和 POD 的,涵盖以下内容:
- 什么是聚合?
- 什么是POD(普通旧数据)?
- 最近,什么是平凡或平凡可复制类型?
- 它们有何关系?
- 它们如何以及为何如此特别?
- C++11 有哪些变化?
This FAQ is about Aggregates and PODs and covers the following material:
- What are aggregates?
- What are PODs (Plain Old Data)?
- More recently, what are trivial or trivially copyable types?
- How are they related?
- How and why are they special?
- What changes for C++11?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
如何阅读:
这篇文章相当长。如果您想了解聚合和 POD(普通旧数据),请花时间阅读。如果您只对聚合感兴趣,请仅阅读第一部分。如果您只对 POD 感兴趣,那么您必须首先阅读聚合的定义、含义和示例,然后您可以跳转到 POD,但我仍然建议您完整阅读第一部分。聚合的概念对于定义 POD 至关重要。如果您发现任何错误(即使是很小的错误,包括语法、文体、格式、语法等),请发表评论,我会编辑。
这个答案适用于C++03。有关其他 C++ 标准,请参阅:
什么是聚合以及为什么它们很特殊
来自 C++ 标准的正式定义 (C++03 8.5.1 §1)< /强>:
那么,好吧,让我们解析一下这个定义。首先,任何数组都是聚合。一个类也可以是一个聚合,如果……等等!没有提到结构或联合,它们不能是聚合吗?是的,他们可以。在 C++ 中,术语
class
指所有类、结构和联合。因此,当且仅当类(或结构或联合)满足上述定义的条件时,它才是聚合。这些标准意味着什么?这并不意味着聚合类不能有构造函数,事实上它可以有默认构造函数和/或复制构造函数,只要它们是由编译器隐式声明的,而不是由用户显式声明的
没有私有或受保护< em>非静态数据成员。您可以根据需要拥有任意数量的私有和受保护的成员函数(但不能是构造函数)以及私有或受保护的静态数据成员和成员函数,并且不违反聚合类的规则
聚合类可以具有用户声明/用户定义的复制赋值运算符和/或析构函数
数组是聚合,即使它是非聚合类类型的数组。
现在让我们看一些例子:
您明白了。现在让我们看看聚合有何特殊之处。与非聚合类不同,它们可以使用大括号
{}
进行初始化。这种初始化语法对于数组来说是众所周知的,我们刚刚了解到它们是聚合。那么,让我们从他们开始吧。类型 array_name[n] = {a1, a2, …, am};
if(m == n)
数组的第 i 个元素用i
初始化
else if(m < n)
数组的前 m 个元素使用 a1、a2、...、am 和其他
n - m 进行初始化
元素,如果可能的话,值初始化(参见下面的术语解释)else if(m > n)
编译器将发出错误
else (这是根本没有指定 n 的情况,例如
int a[] = {1, 2, 3};)
假设数组 (n) 的大小等于 m,因此
int a[] = {1, 2, 3};
相当于int a[3] = { 1, 2, 3};
当标量类型的对象 (
bool
,int
,char
,double< /code>、指针等)是值初始化的,这意味着它是用
0
对该类型进行初始化的(false
对于bool
、0.0
表示double
等)。当具有用户声明的默认构造函数的类类型的对象被值初始化时,将调用其默认构造函数。如果隐式定义默认构造函数,则所有非静态成员都会递归地进行值初始化。这个定义不精确并且有点不正确,但它应该为您提供基本概念。不能对引用进行值初始化。例如,如果类没有适当的默认构造函数,则非聚合类的值初始化可能会失败。数组初始化的示例:
现在让我们看看如何使用大括号初始化聚合类。几乎是一样的方式。我们将按照非静态数据成员在类定义中出现的顺序(根据定义它们都是公共的)来初始化非静态数据成员,而不是数组元素。如果初始值设定项少于成员,则其余的将进行值初始化。如果无法对未显式初始化的成员之一进行值初始化,则会出现编译时错误。如果初始化程序多于必要的数量,我们也会收到编译时错误。
在上面的示例中,
yc
使用'a'
初始化,yxi1
使用10
初始化,yxi2
初始化code> 与20
、yi[0]
与20
、yi[1]
与30< /code>和
yf
是值初始化的,即用0.0
初始化。受保护的静态成员d
根本没有初始化,因为它是static
。聚合联合的不同之处在于,您只能用大括号初始化它们的第一个成员。我认为,如果您在 C++ 方面足够先进,甚至可以考虑使用联合(它们的使用可能非常危险,必须仔细考虑),您可以自己在标准中查找联合规则:)。
现在我们知道了聚合的特殊之处,让我们尝试了解类的限制;也就是说,他们为什么在那里。我们应该理解,用大括号进行成员初始化意味着该类只不过是其成员的总和。如果存在用户定义的构造函数,则意味着用户需要做一些额外的工作来初始化成员,因此大括号初始化将不正确。如果存在虚函数,则意味着该类的对象(在大多数实现上)有一个指向该类所谓的 vtable 的指针,该指针在构造函数中设置,因此大括号初始化是不够的。您可以通过与练习类似的方式找出其余的限制:)。
关于聚合就足够了。现在我们可以定义一组更严格的类型,即 POD
什么是 POD 以及它们为何特殊
来自 C++ 标准的正式定义 (C++03 9 §4):
哇,这个更难解析,不是吗? :) 让我们把联合排除在外(基于与上面相同的理由)并以更清晰的方式重新表述:
这个定义意味着什么? (我有没有提到POD代表普通旧数据?)
示例:
POD 类、POD 联合、标量类型和此类类型的数组统称为POD 类型。
POD 在很多方面都很特殊。我将仅提供一些示例。
POD 类最接近 C 结构。与它们不同的是,POD 可以具有成员函数和任意静态成员,但这两者都不会改变对象的内存布局。因此,如果您想编写一个可在 C 甚至 .NET 中使用的或多或少可移植的动态库,您应该尝试使所有导出函数仅接受和返回 POD 类型的参数。
非 POD 类类型的对象的生命周期从构造函数完成时开始,到析构函数开始时结束。对于 POD 类,生命周期从对象的存储空间被占用时开始,到存储空间被释放或重用时结束。
对于 POD 类型的对象,标准保证当您将对象的内容
memcpy
转换为 char 或 unsigned char 数组,然后memcpy
内容返回到您的对象中,该对象将保留其原始值。请注意,对于非 POD 类型的对象没有这样的保证。此外,您还可以使用memcpy
安全地复制 POD 对象。以下示例假设 T 是 POD 类型:goto 语句。您可能知道,通过 goto 从某个变量尚未在范围内的点跳转到它已在范围内的点是非法的(编译器应该发出错误)。仅当变量为非 POD 类型时,此限制才适用。在以下示例中,
f()
格式错误,而g()
格式良好。请注意,Microsoft 的编译器对此规则过于宽松 — 它只是在这两种情况下发出警告。<前><代码> int f()
{
结构 NonPOD {NonPOD() {}};
转到标签;
非POD x;
标签:
返回0;
}
整数 g()
{
结构 POD {int i;字符 c;};
转到标签;
PODx;
标签:
返回0;
}
保证 POD 对象的开头不会有任何填充。换句话说,如果 POD 类 A 的第一个成员是 T 类型,您可以安全地从
A*
到T*
进行reinterpret_cast
并得到指向第一个成员的指针,反之亦然。这样的例子不胜枚举……
结论
了解 POD 到底是什么非常重要,因为如您所见,许多语言功能对它们的行为有所不同。
How to read:
This article is rather long. If you want to know about both aggregates and PODs (Plain Old Data) take time and read it. If you are interested just in aggregates, read only the first part. If you are interested only in PODs then you must first read the definition, implications, and examples of aggregates and then you may jump to PODs but I would still recommend reading the first part in its entirety. The notion of aggregates is essential for defining PODs. If you find any errors (even minor, including grammar, stylistics, formatting, syntax, etc.) please leave a comment, I'll edit.
This answer applies to C++03. For other C++ standards see:
What are aggregates and why they are special
Formal definition from the C++ standard (C++03 8.5.1 §1):
So, OK, let's parse this definition. First of all, any array is an aggregate. A class can also be an aggregate if… wait! nothing is said about structs or unions, can't they be aggregates? Yes, they can. In C++, the term
class
refers to all classes, structs, and unions. So, a class (or struct, or union) is an aggregate if and only if it satisfies the criteria from the above definitions. What do these criteria imply?This does not mean an aggregate class cannot have constructors, in fact it can have a default constructor and/or a copy constructor as long as they are implicitly declared by the compiler, and not explicitly by the user
No private or protected non-static data members. You can have as many private and protected member functions (but not constructors) as well as as many private or protected static data members and member functions as you like and not violate the rules for aggregate classes
An aggregate class can have a user-declared/user-defined copy-assignment operator and/or destructor
An array is an aggregate even if it is an array of non-aggregate class type.
Now let's look at some examples:
You get the idea. Now let's see how aggregates are special. They, unlike non-aggregate classes, can be initialized with curly braces
{}
. This initialization syntax is commonly known for arrays, and we just learnt that these are aggregates. So, let's start with them.Type array_name[n] = {a1, a2, …, am};
if(m == n)
the ith element of the array is initialized with ai
else if(m < n)
the first m elements of the array are initialized with a1, a2, …, am and the other
n - m
elements are, if possible, value-initialized (see below for the explanation of the term)else if(m > n)
the compiler will issue an error
else (this is the case when n isn't specified at all like
int a[] = {1, 2, 3};
)the size of the array (n) is assumed to be equal to m, so
int a[] = {1, 2, 3};
is equivalent toint a[3] = {1, 2, 3};
When an object of scalar type (
bool
,int
,char
,double
, pointers, etc.) is value-initialized it means it is initialized with0
for that type (false
forbool
,0.0
fordouble
, etc.). When an object of class type with a user-declared default constructor is value-initialized its default constructor is called. If the default constructor is implicitly defined then all nonstatic members are recursively value-initialized. This definition is imprecise and a bit incorrect but it should give you the basic idea. A reference cannot be value-initialized. Value-initialization for a non-aggregate class can fail if, for example, the class has no appropriate default constructor.Examples of array initialization:
Now let's see how aggregate classes can be initialized with braces. Pretty much the same way. Instead of the array elements we will initialize the non-static data members in the order of their appearance in the class definition (they are all public by definition). If there are fewer initializers than members, the rest are value-initialized. If it is impossible to value-initialize one of the members which were not explicitly initialized, we get a compile-time error. If there are more initializers than necessary, we get a compile-time error as well.
In the above example
y.c
is initialized with'a'
,y.x.i1
with10
,y.x.i2
with20
,y.i[0]
with20
,y.i[1]
with30
andy.f
is value-initialized, that is, initialized with0.0
. The protected static memberd
is not initialized at all, because it isstatic
.Aggregate unions are different in that you may initialize only their first member with braces. I think that if you are advanced enough in C++ to even consider using unions (their use may be very dangerous and must be thought of carefully), you could look up the rules for unions in the standard yourself :).
Now that we know what's special about aggregates, let's try to understand the restrictions on classes; that is, why they are there. We should understand that memberwise initialization with braces implies that the class is nothing more than the sum of its members. If a user-defined constructor is present, it means that the user needs to do some extra work to initialize the members therefore brace initialization would be incorrect. If virtual functions are present, it means that the objects of this class have (on most implementations) a pointer to the so-called vtable of the class, which is set in the constructor, so brace-initialization would be insufficient. You could figure out the rest of the restrictions in a similar manner as an exercise :).
So enough about the aggregates. Now we can define a stricter set of types, to wit, PODs
What are PODs and why they are special
Formal definition from the C++ standard (C++03 9 §4):
Wow, this one's tougher to parse, isn't it? :) Let's leave unions out (on the same grounds as above) and rephrase in a bit clearer way:
What does this definition imply? (Did I mention POD stands for Plain Old Data?)
Examples:
POD-classes, POD-unions, scalar types, and arrays of such types are collectively called POD-types.
PODs are special in many ways. I'll provide just some examples.
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 begun. 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 thenmemcpy
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 withmemcpy
. The following example assumes T is a POD-type: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 whereasg()
is well-formed. Note that Microsoft's compiler is too liberal with this rule—it just issues a warning in both cases.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
fromA*
toT*
and get the pointer to the first member and vice versa.The list goes on and on…
Conclusion
It is important to understand what exactly a POD is because many language features, as you see, behave differently for them.
C++11 有哪些变化?
聚合
聚合的标准定义略有变化,但仍然几乎相同:
好吧,什么改变了?
以前,聚合不能有用户声明的构造函数,但现在它不能有用户提供的构造函数。有区别吗?是的,有,因为现在您可以声明构造函数并默认它们:
这仍然是一个聚合,因为第一个声明中默认的构造函数(或任何特殊成员函数)不是用户提供的。
现在,对于非静态数据成员,聚合不能有任何大括号或等于初始化器。这意味着什么?嗯,这只是因为有了这个新标准,我们可以直接在类中初始化成员,如下所示:
使用此功能使类不再是聚合,因为它基本上相当于提供您自己的默认构造函数。
所以,什么是聚合根本没有太大变化。它仍然是相同的基本思想,适应了新功能。
POD 怎么样?
POD 经历了很多变化。在这个新标准中,许多以前有关 POD 的规则都得到了放宽,并且标准中提供定义的方式也发生了根本性的改变。
POD 的想法基本上是捕获两个不同的属性:
因此,定义已分为两个不同的概念:琐碎类和标准布局类,因为它们比 POD 更有用。该标准现在很少使用术语 POD,而是更喜欢更具体的“琐碎”和“标准布局”概念。
新定义基本上表明 POD 是一个既简单又具有标准布局的类,并且此属性必须递归地适用于所有非静态数据成员:
让我们分别详细讨论这两个属性。
平凡类
Trivial 是上面提到的第一个属性:平凡类支持静态初始化。
如果一个类是普通可复制的(普通类的超集),则可以使用诸如memcpy之类的东西将其表示形式复制到该位置,并期望结果相同。
该标准定义了一个普通类,如下所示:
那么,这些琐碎和不琐碎的事情是什么?
基本上,这意味着如果不是用户提供的复制或移动构造函数是微不足道的,该类中没有任何虚拟内容,并且该属性对于该类的所有成员和基类都递归地保留。
普通复制/移动赋值运算符的定义非常相似,只需将“构造函数”一词替换为“赋值运算符”即可。
普通析构函数也有类似的定义,但附加了它不能是虚拟的约束。
对于简单的默认构造函数还存在另一个类似的规则,此外,如果类具有带有大括号或等于初始化器的非静态数据成员,那么默认构造函数就不是简单的,我们'上面已经看过了。
下面是一些示例来澄清一切:
Standard-layout
Standard-layout 是第二个属性。标准提到这些对于与其他语言进行通信非常有用,这是因为标准布局类具有与等效 C 结构或联合相同的内存布局。
这是成员和所有基类必须递归保留的另一个属性。和往常一样,不允许使用任何虚函数或虚基类。这将使布局与 C 不兼容。
这里的一个宽松规则是标准布局类必须让所有非静态数据成员具有相同的访问控制。以前这些必须全部公开,但现在您可以将它们设为私有或受保护,只要它们全部私有或全部受保护。
使用继承时,整个继承树中只有一个类可以拥有非静态数据成员,并且第一个非静态数据成员不能是基类类型(这可能会破坏别名规则),否则,它不是标准布局类。
标准文本中的定义是这样的:
让我们看几个例子。
结论
通过这些新规则,现在更多的类型可以成为 POD。即使类型不是 POD,我们也可以单独利用 POD 的一些属性(如果它只是普通或标准布局之一)。
标准库具有在标头
中测试这些属性的特征:What changes for C++11?
Aggregates
The standard definition of an aggregate has changed slightly, but it's still pretty much the same:
Ok, what changed?
Previously, an aggregate could have no user-declared constructors, but now it can't have user-provided constructors. Is there a difference? Yes, there is, because now you can declare constructors and default them:
This is still an aggregate because a constructor (or any special member function) that is defaulted on the first declaration is not user-provided.
Now an aggregate cannot have any brace-or-equal-initializers for non-static data members. What does this mean? Well, this is just because with this new standard, we can initialize members directly in the class like this:
Using this feature makes the class no longer an aggregate because it's basically equivalent to providing your own default constructor.
So, what is an aggregate didn't change much at all. It's still the same basic idea, adapted to the new features.
What about PODs?
PODs went through a lot of changes. Lots of previous rules about PODs were relaxed in this new standard, and the way the definition is provided in the standard was radically changed.
The idea of a POD is to capture basically two distinct properties:
Because of this, the definition has been split into two distinct concepts: trivial classes and standard-layout classes, because these are more useful than POD. The standard now rarely uses the term POD, preferring the more specific trivial and standard-layout concepts.
The new definition basically says that a POD is a class that is both trivial and has standard-layout, and this property must hold recursively for all non-static data members:
Let's go over each of these two properties in detail separately.
Trivial classes
Trivial is the first property mentioned above: trivial classes support static initialization.
If a class is trivially copyable (a superset of trivial classes), it is ok to copy its representation over the place with things like
memcpy
and expect the result to be the same.The standard defines a trivial class as follows:
So, what are all those trivial and non-trivial things?
Basically this means that a copy or move constructor is trivial if it is not user-provided, the class has nothing virtual in it, and this property holds recursively for all the members of the class and for the base class.
The definition of a trivial copy/move assignment operator is very similar, simply replacing the word "constructor" with "assignment operator".
A trivial destructor also has a similar definition, with the added constraint that it can't be virtual.
And yet another similar rule exists for trivial default constructors, with the addition that a default constructor is not-trivial if the class has non-static data members with brace-or-equal-initializers, which we've seen above.
Here are some examples to clear everything up:
Standard-layout
Standard-layout is the second property. The standard mentions that these are useful for communicating with other languages, and that's because a standard-layout class has the same memory layout of the equivalent C struct or union.
This is another property that must hold recursively for members and all base classes. And as usual, no virtual functions or virtual base classes are allowed. That would make the layout incompatible with C.
A relaxed rule here is that standard-layout classes must have all non-static data members with the same access control. Previously these had to be all public, but now you can make them private or protected, as long as they are all private or all protected.
When using inheritance, only one class in the whole inheritance tree can have non-static data members, and the first non-static data member cannot be of a base class type (this could break aliasing rules), otherwise, it's not a standard-layout class.
This is how the definition goes in the standard text:
And let's see a few examples.
Conclusion
With these new rules a lot more types can be PODs now. And even if a type is not POD, we can take advantage of some of the POD properties separately (if it is only one of trivial or standard-layout).
The standard library has traits to test these properties in the header
<type_traits>
:C++14 发生了什么变化
我们可以参考 Draft C++ 14标准供参考。
聚合
这在
8.5.1
部分中有介绍。聚合 为我们提供了以下定义:现在唯一的变化是添加类内成员初始值设定项不会使类成为非聚合类。因此,以下示例来自 C++11 具有成员就地初始化程序的类的聚合初始化:
不是 C 中的聚合++11 但它是 C++14 中的。 N3605:成员初始值设定项和聚合中介绍了此更改,其摘要如下:
POD 保持不变
POD(普通旧数据)结构的定义在
9
Classes 节中介绍,其中表示:与 C++11 的措辞相同。
C++14 的标准布局更改
正如评论中所述,pod 依赖于 标准布局 的定义,并且 C++14 确实发生了更改,但这是通过事后应用于 C++14 的缺陷报告。
共有三个 DR:
所以标准布局 来自 Pre C++14:
至 C++14 中的此内容:
What has changed for C++14
We can refer to the Draft C++14 standard for reference.
Aggregates
This is covered in section
8.5.1
Aggregates which gives us the following definition:The only change is now adding in-class member initializers does not make a class a non-aggregate. So the following example from C++11 aggregate initialization for classes with member in-place initializers:
was not an aggregate in C++11 but it is in C++14. This change is covered in N3605: Member initializers and aggregates, which has the following abstract:
POD stays the same
The definition for POD(plain old data) struct is covered in section
9
Classes which says:which is the same wording as C++11.
Standard-Layout Changes for C++14
As noted in the comments pod relies on the definition of standard-layout and that did change for C++14 but this was via defect reports that were applied to C++14 after the fact.
There were three DRs:
So standard-layout went from this Pre C++14:
To this in C++14:
C++17 中的更改
下载 C++17 国际标准最终草案 此处。
聚合
C++17 扩展并增强了聚合和聚合初始化。标准库现在还包含一个 std::is_aggregate 类型特征类。以下是第 11.6.1.1 和 11.6.1.2 节的正式定义(内部参考已省略):
发生了什么变化?
平凡类
平凡类的定义在 C++17 中进行了重新设计,以解决 C++14 中未解决的几个缺陷。这些变化本质上是技术性的。这是 12.0.6 的新定义(内部引用已删除):
更改:
std::memcpy
合法地复制/移动它。这是一个语义矛盾,因为通过将所有构造函数/赋值运算符定义为已删除,类的创建者明确表示该类不能被复制/移动,但该类仍然满足普通可复制类的定义。因此,在 C++17 中,我们有一个新子句,声明可平凡复制的类必须至少有一个平凡的、不可删除的(尽管不一定可公开访问)复制/移动构造函数/赋值运算符。请参阅N4148,DR1734标准布局类
标准布局的定义也进行了修改以解决缺陷报告。同样,这些变化本质上是技术性的。这是标准 (12.0.7) 中的文本。和以前一样,内部引用被省略:
更改:
注意: C++ 标准委员会打算将基于缺陷报告的上述更改应用于 C++14,尽管新语言并未包含在已发布的 C++14 标准中。它符合 C++17 标准。
Changes in C++17
Download the C++17 International Standard final draft here.
Aggregates
C++17 expands and enhances aggregates and aggregate initialization. The standard library also now includes an
std::is_aggregate
type trait class. Here is the formal definition from section 11.6.1.1 and 11.6.1.2 (internal references elided):What changed?
Trivial Classes
The definition of trivial class was reworked in C++17 to address several defects that were not addressed in C++14. The changes were technical in nature. Here is the new definition at 12.0.6 (internal references elided):
Changes:
std::memcpy
. This was a semantic contradiction, because, by defining as deleted all constructor/assignment operators, the creator of the class clearly intended that the class could not be copied/moved, yet the class still met the definition of a trivially copyable class. Hence in C++17 we have a new clause stating that trivially copyable class must have at least one trivial, non-deleted (though not necessarily publicly accessible) copy/move constructor/assignment operator. See N4148, DR1734Standard-layout Classes
The definition of standard-layout was also reworked to address defect reports. Again the changes were technical in nature. Here is the text from the standard (12.0.7). As before, internal references are elided:
Changes:
Note: The C++ standards committee intended the above changes based on defect reports to apply to C++14, though the new language is not in the published C++14 standard. It is in the C++17 standard.
C++11 中的 POD 基本上分为两个不同的轴:琐碎性和布局。琐碎性是指对象的概念值与其存储中的数据位之间的关系。布局是关于……嗯,一个对象的子对象的布局。只有类类型具有布局,而所有类型都具有琐碎关系。
因此,琐碎性轴的含义如下:
非平凡可复制:此类类型的对象的值可能不仅仅是直接存储在对象内的二进制数据。
例如,
unique_ptr
存储一个T*
;这是对象内二进制数据的总体。但这并不是unique_ptr
的值的全部。unique_ptr
存储nullptr
或指向其生命周期由unique_ptr
实例管理的对象的指针。该管理是unique_ptr
的值的一部分。并且该值不是对象的二进制数据的一部分;它是由该对象的各种成员函数创建的。例如,将
nullptr
分配给unique_ptr
不仅仅是更改对象中存储的位。这样的赋值必须销毁由unique_ptr
管理的任何对象。操作unique_ptr
的内部存储而不通过它的成员函数会破坏这个机制,改变它的内部T*
而不破坏它当前管理的对象,会违反对象拥有的概念价值。可简单复制:此类对象的值恰好且仅是其二进制存储的内容。这就是允许复制二进制存储等同于复制对象本身的合理性。
定义普通可复制性(普通析构函数、普通/删除复制/移动构造函数/赋值)的特定规则是类型仅包含二进制值所需的规则。对象的析构函数可以参与定义对象的“值”,就像
unique_ptr
的情况一样。如果该析构函数很简单,那么它不会参与定义对象的值。专门的复制/移动操作也可以参与对象的值。
unique_ptr
的移动构造函数通过将移动操作的源清空来修改它。这可以确保unique_ptr
的值是唯一。简单的复制/移动操作意味着不会玩这种对象值恶作剧,因此对象的值只能是它存储的二进制数据。平凡:该对象被认为对其存储的任何位都具有功能值。简单可复制将对象的数据存储的含义定义为该数据。但这些类型仍然可以控制数据如何到达那里(在某种程度上)。此类类型可以具有默认成员初始值设定项和/或默认构造函数,以确保特定成员始终具有特定值。因此,对象的概念值可以限制为它可以存储的二进制数据的子集。
对具有简单默认构造函数的类型执行默认初始化将使该对象具有完全未初始化的值。因此,具有简单默认构造函数的类型对于其数据存储中的任何二进制数据在逻辑上都是有效的。
布局轴确实非常简单。编译器在决定类的子对象如何存储在类的存储中时有很大的余地。然而,在某些情况下,这种余地是不必要的,并且具有更严格的订购保证是有用的。
此类类型是标准布局类型。 C++ 标准甚至没有真正说明布局的具体含义。它基本上说明了有关标准布局类型的三件事:
第一个子对象与对象本身位于同一地址。
您可以使用
offsetof
获取从外部对象到其成员子对象之一的字节偏移量。union
如果活动成员(至少部分)使用与正在访问的非活动成员相同的布局,则可以通过联合体的非活动成员访问子对象来玩一些游戏。编译器通常允许标准布局对象映射到具有与 C 中相同成员的 struct 类型。但 C++ 标准中没有对此进行说明;这正是编译器想做的事情。
在这一点上,POD 基本上是一个无用的术语。它只是简单的可复制性(值只是其二进制数据)和标准布局(其子对象的顺序更明确定义)的交集。从这些事情中我们可以推断出该类型是类似 C 的并且可以映射到类似的 C 对象。但该标准没有对此做出任何声明。
我会尝试:
这很简单:所有非静态数据成员必须全部为
public
、私有
,或受保护
。您不能有一些公共
和一些私有
。它们的推理完全取决于“标准布局”和“非标准布局”之间的区别。也就是说,让编译器可以自由选择如何将内容放入内存。这不仅仅是虚函数表指针的问题。
当他们在 98 年标准化 C++ 时,他们必须基本上预测人们将如何实现它。虽然他们对各种 C++ 风格拥有相当多的实现经验,但他们对事情并不确定。因此他们决定谨慎行事:给编译器尽可能多的自由。
这就是为什么C++98中POD的定义如此严格。它为 C++ 编译器提供了大多数类的成员布局方面的很大自由度。基本上,POD 类型是为了特殊情况而设计的,是出于某种原因而专门编写的。
当开发 C++11 时,他们在编译器方面拥有更多经验。他们意识到……C++ 编译器编写者真的很懒。他们拥有所有这些自由,但他们没有用它做任何事情。
标准布局的规则或多或少地编码了常见的做法:大多数编译器实际上不需要做太多改变来实现它们(除了相应类型特征的一些东西之外)。
现在,当谈到
public
/private
时,情况就不同了。自由地重新排序哪些成员是公共的,哪些是私有的,实际上对编译器很重要,特别是在调试构建时。由于标准布局的要点是与其他语言兼容,因此您不能让调试与发布中的布局有所不同。事实上,它并没有真正伤害用户。如果您正在创建一个封装类,那么您的所有数据成员很可能都是
私有
。您通常不会公开完全封装类型的公共数据成员。因此,对于那些确实想要这样做、想要进行这种划分的少数用户来说,这只会是一个问题。所以损失并不大。
这一点的原因又回到了为什么他们再次标准化标准布局:常见做法。
在继承树中拥有两个实际存储内容的成员时,没有常见的做法。有些将基类放在派生类之前,有些则相反。如果成员来自两个基类,您将如何排序?等等。编译器在这些问题上存在很大分歧。
另外,由于零/一/无穷大规则,一旦你说你可以有两个包含成员的班级,你就可以说任意多个。这需要添加很多布局规则来处理这个问题。你必须说明多重继承是如何工作的,哪些类将它们的数据放在其他类之前,等等。这是很多规则,但获得的物质收益却很少。
您无法将没有虚函数和默认构造函数的所有内容都制作为标准布局。
我真的无法和这个人说话。我对 C++ 别名规则的了解还不够,无法真正理解它。但这与基成员将与基类本身共享相同的地址这一事实有关。那就是:
这可能违反 C++ 的别名规则。在某种程度上。
然而,请考虑一下:拥有执行此操作的能力实际上会有多大用处?由于只有一个类可以具有非静态数据成员,因此
Derived
必须是该类(因为它具有Base
作为成员)。因此Base
必须为空(数据)。如果Base
为空,以及一个基类...为什么要有它的数据成员呢?由于
Base
为空,因此它没有状态。因此,任何非静态成员函数都会根据其参数而不是this
指针执行其操作。再说一遍:没有大损失。
POD in C++11 was basically split into two different axes here: triviality and layout. Triviality is about the relationship between an object's conceptual value and the bits of data within its storage. Layout is about... well, the layout of an object's subobjects. Only class types have layout, while all types have triviality relationships.
So here is what the triviality axis is about:
Non-trivially copyable: The value of objects of such types may be more than just the binary data that are stored directly within the object.
For example,
unique_ptr<T>
stores aT*
; that is the totality of the binary data within the object. But that's not the totality of the value of aunique_ptr<T>
. Aunique_ptr<T>
stores either anullptr
or a pointer to an object whose lifetime is managed by theunique_ptr<T>
instance. That management is part of the value of aunique_ptr<T>
. And that value is not part of the binary data of the object; it is created by the various member functions of that object.For example, to assign
nullptr
to aunique_ptr<T>
is to do more than just change the bits stored in the object. Such an assignment must destroy any object managed by theunique_ptr
. To manipulate the internal storage of aunique_ptr
without going through its member functions would damage this mechanism, to change its internalT*
without destroying the object it currently manages, would violate the conceptual value that the object possesses.Trivially copyable: The value of such objects are exactly and only the contents of their binary storage. This is what makes it reasonable to allow copying that binary storage to be equivalent to copying the object itself.
The specific rules that define trivial copyability (trivial destructor, trivial/deleted copy/move constructors/assignment) are what is required for a type to be binary-value-only. An object's destructor can participate in defining the "value" of an object, as in the case with
unique_ptr
. If that destructor is trivial, then it doesn't participate in defining the object's value.Specialized copy/move operations also can participate in an object's value.
unique_ptr
's move constructor modifies the source of the move operation by null-ing it out. This is what ensures that the value of aunique_ptr
is unique. Trivial copy/move operations mean that such object value shenanigans are not being played, so the object's value can only be the binary data it stores.Trivial: This object is considered to have a functional value for any bits that it stores. Trivially copyable defines the meaning of the data store of an object as being just that data. But such types can still control how data gets there (to some extent). Such a type can have default member initializers and/or a default constructor that ensures that a particular member always has a particular value. And thus, the conceptual value of the object can be restricted to a subset of the binary data that it could store.
Performing default initialization on a type that has a trivial default constructor will leave that object with completely uninitialized values. As such, a type with a trivial default constructor is logically valid with any binary data in its data storage.
The layout axis is really quite simple. Compilers are given a lot of leeway in deciding how the subobjects of a class are stored within the class's storage. However, there are some cases where this leeway is not necessary, and having more rigid ordering guarantees is useful.
Such types are standard layout types. And the C++ standard doesn't even really do much with saying what that layout is specifically. It basically says three things about standard layout types:
The first subobject is at the same address as the object itself.
You can use
offsetof
to get a byte offset from the outer object to one of its member subobjects.union
s get to play some games with accessing subobjects through an inactive member of a union if the active member is (at least partially) using the same layout as the inactive one being accessed.Compilers generally permit standard layout objects to map to
struct
types with the same members in C. But there is no statement of that in the C++ standard; that's just what compilers feel like doing.POD is basically a useless term at this point. It is just the intersection of trivial copyability (the value is only its binary data) and standard layout (the order of its subobjects is more well-defined). One can infer from such things that the type is C-like and could map to similar C objects. But the standard has no statements to that effect.
I'll try:
That's simple: all non-static data members must all be
public
,private
, orprotected
. You can't have somepublic
and someprivate
.The reasoning for them goes to the reasoning for having a distinction between "standard layout" and "not standard layout" at all. Namely, to give the compiler the freedom to choose how to put things into memory. It's not just about vtable pointers.
Back when they standardized C++ in 98, they had to basically predict how people would implement it. While they had quite a bit of implementation experience with various flavors of C++, they weren't certain about things. So they decided to be cautious: give the compilers as much freedom as possible.
That's why the definition of POD in C++98 is so strict. It gave C++ compilers great latitude on member layout for most classes. Basically, POD types were intended to be special cases, something you specifically wrote for a reason.
When C++11 was being worked on, they had a lot more experience with compilers. And they realized that... C++ compiler writers are really lazy. They had all this freedom, but they didn't do anything with it.
The rules of standard layout are more or less codifying common practice: most compilers didn't really have to change much if anything at all to implement them (outside of maybe some stuff for the corresponding type traits).
Now, when it came to
public
/private
, things are different. The freedom to reorder which members arepublic
vs.private
actually can matter to the compiler, particularly in debugging builds. And since the point of standard layout is that there is compatibility with other languages, you can't have the layout be different in debug vs. release.Then there's the fact that it doesn't really hurt the user. If you're making an encapsulated class, odds are good that all of your data members will be
private
anyway. You generally don't expose public data members on fully encapsulated types. So this would only be a problem for those few users who do want to do that, who want that division.So it's no big loss.
The reason for this one comes back to why they standardized standard layout again: common practice.
There's no common practice when it comes to having two members of an inheritance tree that actually store things. Some put the base class before the derived, others do it the other way. Which way do you order the members if they come from two base classes? And so on. Compilers diverge greatly on these questions.
Also, thanks to the zero/one/infinity rule, once you say you can have two classes with members, you can say as many as you want. This requires adding a lot of layout rules for how to handle this. You have to say how multiple inheritance works, which classes put their data before other classes, etc. That's a lot of rules, for very little material gain.
You can't make everything that doesn't have virtual functions and a default constructor standard layout.
I can't really speak to this one. I'm not educated enough in C++'s aliasing rules to really understand it. But it has something to do with the fact that the base member will share the same address as the base class itself. That is:
And that's probably against C++'s aliasing rules. In some way.
However, consider this: how useful could having the ability to do this ever actually be? Since only one class can have non-static data members, then
Derived
must be that class (since it has aBase
as a member). SoBase
must be empty (of data). And ifBase
is empty, as well as a base class... why have a data member of it at all?Since
Base
is empty, it has no state. So any non-static member functions will do what they do based on their parameters, not theirthis
pointer.So again: no big loss.
c++20< 中有哪些变化/a>
遵循这个问题的明确主题的其余部分,聚合的含义和使用随着每个标准的不同而不断变化。即将发生几个关键的变化。
具有用户声明构造函数的类型 P1008
在 C++17 中,此类型仍然是聚合:
因此,
X{}
仍然可以编译,因为这是聚合初始化 - 而不是构造函数调用。另请参阅:什么时候私有构造函数不是私有构造函数?在 C++20 中,限制将从要求更改为:
函数
这已被采纳到 C++20 工作中草稿。这里的
X
和链接问题中的C
都不会在 C++20 中聚合。这也造成了溜溜球效应,如下例所示:
在 C++11/14 中,由于基类的原因,
B
不是聚合,因此>B{}
执行值初始化,调用B::B()
调用A::A()
,在可访问的位置。这是格式良好的。在 C++17 中,
B
成为聚合,因为允许基类,这使得B{}
聚合初始化。这需要从{}
复制列表初始化A
,但要从B
上下文外部进行复制列表初始化,因为在该位置它是不可访问的。在 C++17 中,这是格式错误的(尽管auto x = B();
也可以)。现在在 C++20 中,由于上述规则更改,
B
再次不再是聚合(不是因为基类,而是因为用户声明的默认构造函数 - 即使它是默认)。所以我们回到 B 的构造函数,并且这个片段变得格式良好。从带括号的值列表初始化聚合 P960
出现的一个常见问题是想要使用
emplace()具有聚合的
样式构造函数:这不起作用,因为
emplace
将尝试有效地执行初始化X(1, 2)
,这是无效的。典型的解决方案是向X
添加一个构造函数,但通过此提议(目前正在通过 Core 进行),聚合将有效地合成了能够执行正确操作的构造函数 - 并且行为与常规构造函数类似。上面的代码将在 C++20 中按原样编译。聚合的类模板参数推导 (CTAD) P1021 (特别是 P1816)
在 C++17 中,这无法编译:
用户必须为所有聚合模板编写自己的推导指南:
但这在某种意义上是“显而易见的事情” “要做的事情,基本上只是样板,语言会为你做这件事。此示例将以 C++20 进行编译(无需用户提供的推导指南)。
What changes in c++20
Following the rest of the clear theme of this question, the meaning and use of aggregates continues to change with every standard. There are several key changes on the horizon.
Types with user-declared constructors P1008
In C++17, this type is still an aggregate:
And hence,
X{}
still compiles because that is aggregate initialization - not a constructor invocation. See also: When is a private constructor not a private constructor?In C++20, the restriction will change from requiring:
to
This has been adopted into the C++20 working draft. Neither the
X
here nor theC
in the linked question will be aggregates in C++20.This also makes for a yo-yo effect with the following example:
In C++11/14,
B
was not an aggregate due to the base class, soB{}
performs value-initialization which callsB::B()
which callsA::A()
, at a point where it is accessible. This was well-formed.In C++17,
B
became an aggregate because base classes were allowed, which madeB{}
aggregate-initialization. This requires copy-list-initializing anA
from{}
, but from outside the context ofB
, where it is not accessible. In C++17, this is ill-formed (auto x = B();
would be fine though).In C++20 now, because of the above rule change,
B
once again ceases to be an aggregate (not because of the base class, but because of the user-declared default constructor - even though it's defaulted). So we're back to going throughB
's constructor, and this snippet becomes well-formed.Initializing aggregates from a parenthesized list of values P960
A common issue that comes up is wanting to use
emplace()
-style constructors with aggregates:This does not work, because
emplace
will try to effectively perform the initializationX(1, 2)
, which is not valid. The typical solution is to add a constructor toX
, but with this proposal (currently working its way through Core), aggregates will effectively have synthesized constructors which do the right thing - and behave like regular constructors. The above code will compile as-is in C++20.Class Template Argument Deduction (CTAD) for Aggregates P1021 (specifically P1816)
In C++17, this does not compile:
Users would have to write their own deduction guide for all aggregate templates:
But as this is in some sense "the obvious thing" to do, and is basically just boilerplate, the language will do this for you. This example will compile in C++20 (without the need for the user-provided deduction guide).