该常见问题是关于聚合和POD的,涵盖以下材料:


什么是聚合?
什么是POD(普通旧数据)?
它们有什么关系?
它们为何以及为什么如此特别?
C ++ 11会发生什么变化?


评论

什么是POD子集:stackoverflow.com/questions/146452/what-are-pod-types-in-c

可以说这些定义背后的动机大致是:POD ==可存储,聚合==聚合可初始化?

#1 楼

如何阅读:

这篇文章相当长。如果您想了解聚合和POD(普通旧数据),请花一些时间阅读。如果您仅对聚合感兴趣,则仅阅读第一部分。如果仅对POD感兴趣,则必须先阅读聚合的定义,含义和示例,然后再跳到POD,但我仍建议您完整阅读第一部分。聚合的概念对于定义POD至关重要。如果发现任何错误(包括语法,文体,格式,语法等,甚至是很小的错误),请发表评论,我将进行编辑。

此答案适用于C ++ 03。有关其他C ++标准,请参见:


C ++ 11的变化
C ++ 14的变化
C ++ 17的变化

什么是聚合,为什么它们很特殊

C ++标准的正式定义(C ++ 03 8.5.1§1):


聚合是一个数组或没有用户声明的
构造函数(12.1),没有私有或受保护的非静态数据成员(第11章),没有基类(第10节)且没有虚拟的类(第9节)函数(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};

if(m == n)
数组的第ith个元素用aielse if(m 用a1,a2,…,am初始化数组的前m个元素,并且如果可能的话,其他n - m元素进行值初始化(请参见下面的术语解释),否则if(m> n)
编译器将发出errorelse(当像int a[] = {1, 2, 3};一样根本没有指定n时,就是这种情况)
假定数组(n)的大小等于m,所以int a[] = {1, 2, 3};等价于int a[3] = {1, 2, 3};

当标量类型的对象(boolintchardouble,指针等)被值初始化时,这意味着它使用0进行了该类型的初始化(false表示bool0.0表示double等)。使用用户声明的默认构造函数对类类型的对象进行值初始化时,将调用其默认构造函数。如果隐式定义了默认构造函数,则将对所有非静态成员进行递归值初始化。这个定义是不精确的,有点不正确,但它应该为您提供基本概念。引用不能进行值初始化。非聚合类的值初始化可能会失败,例如,如果该类没有适当的默认构造函数。

数组初始化的示例:

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初始化为10y.x.i2使用20y.i[0]20y.i[1]30y.f进行值初始化,即用0.0初始化。受保护的静态成员d根本没有初始化,因为它是static

聚合联合的不同之处在于,您可以仅使用大括号初始化其第一个成员。我认为,如果您在C ++方面具有足够的先进水平,甚至可以考虑使用并集(它们的使用可能非常危险,必须仔细考虑),则可以自己在标准中查找并集的规则:)。

现在我们知道聚合的特殊之处,让我们尝试了解对类的限制;那就是为什么他们在那里。我们应该理解,使用大括号的成员方式初始化意味着该类不过是其成员的总和。如果存在用户定义的构造函数,则意味着用户需要做一些额外的工作来初始化成员,因此括号初始化将是不正确的。如果存在虚函数,则意味着该类的对象(在大多数实现中)具有指向该类的所谓vtable的指针,该指针在构造函数中设置,因此花括号初始化将不够。您可以通过练习类似的方式找出其余的限制:)。

关于聚合的内容已经足够了。现在我们可以定义一组更严格的类型,例如POD

什么是POD以及为什么它们是特殊的

来自C ++标准的正式定义(C ++ 03 9 §4):


POD结构是一个聚合类
,没有
类型的非静态数据成员non-POD-struct,non- POD-union(或此类数组)或引用,并且
没有用户定义的副本分配操作符,也没有用户定义的析构函数。同样,POD-union是
的聚合联合,其中没有
类型的非静态数据成员
non-POD-struct,non-POD-union(或
数组)或引用),并且
没有用户定义的副本分配
运算符,也没有用户定义的
析构函数。 POD类是一个
类,它是一个POD结构或一个
POD联合。


哇,这很难解析,不是吗? :)让我们省去工会(基于与上述相同的理由),并以更清晰的方式重新措词:


如果聚合类没有用户,则称为POD定义的copy-assignment
运算符和析构函数,并且
它的非静态成员都不是非POD
类,非POD数组或
引用。 >

这个定义意味着什么? (我提到过POD代表普通旧数据吗?)


所有POD类都是聚合的,或者反之,如果一个类不是聚合的,那么它就是即使两种情况的标准术语都是POD-struct,也不能肯定POD
类与struct一样可以是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联合,标量类型以及此类类型的数组统称为POD-类型。
POD在许多方面都很特殊。我仅提供一些示例。


POD类是最接近C结构的类。与它们不同,POD可以具有成员函数和任意静态成员,但是这两个都不能更改对象的内存布局。因此,如果您想编写一个或多或少可移植的动态库,可以在C甚至.NET中使用,则应尝试使所有导出的函数都采用并仅返回POD类型的参数。
非POD类类型的对象在构造函数完成时开始,在析构函数完成时结束。对于POD类,生存期从对象的存储被占用开始,到释放或重用该存储时结束。

对于POD类型的对象,该标准保证,当您将对象的内容memcpy放入char或未签名的char数组中,然后memcpy将内容返回给您的对象时,该对象将保留其原始值。请注意,对于非POD类型的对象没有这样的保证。另外,您可以使用memcpy安全地复制POD对象。以下示例假定T为POD类型:

#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从尚未在某个范围内的变量跳转到已经在范围内的点是非法的(编译器应发出错误)。仅当变量为非POD类型时,此限制才适用。在以下示例中,f()格式错误,而g()格式正确。请注意,Microsoft的编译器在使用此规则时过于宽松-在这两种情况下都只会发出警告。

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类A的第一个成员是T类型,则可以安全地将reinterpret_castA*更改为T*,并获得指向第一个成员的指针,反之亦然。

该列表继续,然后关于……

结论

重要的是要了解POD的确切含义,因为如您所见,许多语言功能对于它们来说都有不同的表现。

评论


好答案。注释:“如果隐式定义了默认构造函数,则所有非静态成员都将递归地进行值初始化。”和“非聚合类的值初始化可能会失败,例如,如果该类没有适当的默认构造函数。”是不正确的:使用隐式声明的默认构造函数初始化类的值并不一定需要隐式定义的默认构造函数。因此,给定(插入private:视情况而定):struct A {int const a; };那么A()格式正确,即使A的默认构造函数定义格式不正确。

– Johannes Schaub-小人
2010-11-14 15:52



@Kev:如果您设法将相同的信息打包成一个简短的答案,我们都会很乐意对其进行投票!

–sbi
2010-11-14 15:55

@Armen还会注意您可以对同一问题进行多个回答。每个答案都可以包含该问题解决方案的一部分。在我看来,拧上那些已被认可的东西:)

– Johannes Schaub-小人
2010-11-14 17:10



答案很好。我仍然有一段时间重新浏览这篇文章。顺便说一下有关Visual Studio的警告。如前所述,pod的“ goto语句”对MSVC编译器无知。但是对于switch / case语句,它将生成编译错误。基于此概念,我做了一些测试荚检查器:stackoverflow.com/questions/12232766/test-for-pod-ness-in-c-c11 / ...

–bruziuz
2014年1月7日13:28



以“非POD类类型的对象的生存期在构造函数完成时开始,在析构函数完成时结束”开头的要点中。最后一部分应该改为“当析构函数启动时”。

– Quokka
17年9月16日在18:04

#2 楼

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更有用。现在,标准很少使用术语POD,而是更具体的琐碎和标准布局概念。

新定义基本上说POD是既琐碎又具有标准布局的类,并且此属性必须对所有非静态数据成员递归保存:


POD结构是一个非联盟类,既是一个琐碎的类又是一个标准布局类,
并且没有类型为非POD结构,非POD联合(或此类数组)的非静态数据成员。
类似地,POD联合是既是普通类又是标准布局的联合类,并且没有

没有类型为非POD结构,非POD联合(或此类数组)的非静态数据成员。
POD类是既是POD结构又是POD结构的类。 POD联合。


我们分别详细介绍这两个属性。

私有类

私有是第一个上面提到的属性:普通类支持静态初始化tion。
如果一个类是普通可复制的(普通类的超集),则可以使用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的类类型(或其数组)的非静态数据成员,选择用来复制/移动该成员的构造函数
是微不足道的;

否则,复制/移动构造函数为non -trivial。


基本上,这意味着如果未提供用户提供的副本或移动构造函数,则该类也不是琐碎的,该类中没有虚拟对象,并且此属性对于所有类的成员和基类的成员。

定义平凡的复制/移动赋值运算符的位置非常相似,只需将“构造函数”一词替换为“赋值运算符”即可。

平凡的析构函数也具有类似的定义,但附加的约束是它可以不是虚拟的。

对于琐碎的默认构造函数还有另一个类似的规则,另外,如果类具有带有大括号或等于的非静态数据成员,则默认构造函数不是平凡的-initializers,我们已经在上面看到过。

这里有一些示例可以清除所有内容:

// 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不兼容。

这里的宽松规则是,标准布局类必须使所有非静态数据成员具有相同的访问控制。以前这些必须全部公开,但是现在您可以将它们设置为私有或受保护,只要它们都是私有或全部受保护。

使用继承时,整个继承树中只有一个类可以具有非静态数据成员,并且第一个非静态数据成员不能为基类类型(这可能会破坏别名规则),否则,它不是标准布局类。

这是该定义在标准文本中的显示方式:


标准布局类是这样的类:

—没有类型为non的非静态数据成员-standard-layout类(或此类数组)
或引用,

—没有虚拟函数(10.3)和虚拟基类(10.1),

—对所有非静态数据成员具有相同的访问控制(第11条),

—没有非标准布局的基类,

—都具有在最大派生类和最多一个基类中,非静态数据成员,或者没有具有非静态数据成员的基类,并且

—没有与第一个非静态数据成员相同类型的基类。

标准布局结构是使用class-key结构或
class-key类定义的标准布局类。

标准布局联合是一个标准用class-key联合定义的-layout类。

[注意:标准布局类对于与其他编程语言编写的代码进行通信很有用。它们的布局在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,我们也可以单独利用某些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;


评论


您能否详细说明以下规则:a)标准布局类必须使所有非静态数据成员具有相同的访问控制; b)整个继承树中只有一个类可以具有非静态数据成员,并且第一个非静态数据成员不能属于基类类型(这可能会破坏别名规则)。特别是它们的原因是什么?对于后面的规则,您能否提供一个打破混叠的示例?

–安德烈(Andriy Tylychko)
2011-09-10 9:16

@AndyT:看我的答案。我尽我所能回答。

–尼科尔·波拉斯(Nicol Bolas)
2012年2月28日在23:26

可能要针对C ++ 14进行更新,该版本删除了聚合的“无括号或相等初始化器”要求。

– T.C.
14年8月26日在8:01

@ T.C。感谢您的单挑。我将尽快查找这些更改并进行更新。

– R. Martinho Fernandes
14年8月26日在12:18

关于别名:有一个C ++布局规则,如果类C具有一个(空的)基X,并且C的第一个数据成员是类型X,则该第一个成员不能与基X处于相同的偏移量;如果需要避免它,它将在其前面获得一个伪填充字节。在相同的地址处有两个X(或子类)实例可能会破坏需要通过其地址来区分不同实例的事物(一个空实例没有其他东西可以区分它……)。在任何情况下,放入该填充字节的需求都会破坏“布局兼容”。

– greggo
18 Mar 20 '18在22:49

#3 楼

C ++ 14发生了什么变化
我们可以参考C ++ 14草案标准作为参考。
聚合
8.5.1聚合部分中对此进行了介绍,该分类为我们提供了以下定义:

聚合是一个数组或一个类(第9条),没有用户提供的
构造函数(12.1),没有私有或受保护的非静态数据成员
(第11条) ,没有基类(第10章),也没有虚函数
(10.3)。

现在唯一的变化是添加类内成员初始化器不会使类成为非聚合类。因此,以下C ++ 11中的示例使用成员就位初始化程序聚合了类的初始化:
struct A
{
  int a = 3;
  int b = 3;
};

在C ++ 11中不是聚合,而在C ++ 14中。 N3605:成员初始化程序和聚合中涵盖了此更改,该摘要具有以下摘要:

Bjarne Stroustrup和Richard Smith提出了一个关于聚合初始化和成员初始化器不能一起使用的问题。本文
建议采用Smith提出的措辞
来解决此问题,该措辞消除了聚合不能具有成员初始化程序的限制。

POD保持不变
9类中介绍了POD(普通旧数据)结构的定义,该类说:

POD struct110是一个非联盟类,既是一个琐碎的类,又是
标准布局类,并且没有类型为
非POD结构,非POD联合(或此类数组)的非静态数据成员。同样,
POD联合是一个既是普通类又是
standard-layout类的联合,并且没有类型为
non-POD struct,non- POD联合(或此类数组)。 POD类是
是POD结构或POD联合的类。

与C ++ 11的措辞相同。
C的标准布局更改++ 14
正如评论窗格中所指出的,C ++ 14确实依赖于标准布局的定义,但确实发生了变化,但这是通过在事实发生后应用于C ++ 14的缺陷报告来实现的。
有三个DR:

DR 1672
DR 1813
DR 2120

因此标准版图来自此Pre C ++ 14:

标准布局类是这样的类:

(7.1)没有非标准布局类(或此类数组)或引用类型的非静态数据成员,
(7.2)没有虚拟函数([class.virtual]),也没有虚拟基类([class.mi]),
(7.3)对所有对象都具有相同的访问控制(子句[class.access])非静态数据成员,
(7.4)没有非标准布局的基类,
(7.5)在大多数派生类中和最多一个具有非静态数据成员,或者没有具有非静态数据成员的基类,并且
(7.6)没有与第一个非静态数据成员相同类型的基类.109


在C ++ 14中为此:

如果满足以下条件,则类S是标准布局类:

(3.1)没有类型为non-standard-layout的类(或此类数组)或引用的非静态数据成员,
(3.2)没有虚拟函数,也没有虚拟基类,
( 3.3)对所有非静态数据成员具有相同的访问控制,
(3.4)没有非标准布局的基类,
(3.5)最多具有任何给定类型的一个基类子对象,
(3.6)具有该类中的所有非静态数据成员和位字段,并且其基类首先在同一类中声明,并且
(3.7)没有集合M(S )作为基本类的类型,对于任何类型X,M(X)的定义如下。104
[注意:M(X)是所有非基本类子对象的类型集合,这些子对象具有以下特征:
-注释
]

(3.7.1)如果X是不具有(可能继承的)non的非联合类类型-static数据成员,集合M(X)为空。
(3.7.2)如果X是非联合类类型,其非静态数据成员的类型为X0,其大小为零,或者是X的第一个
非静态数据成员(其中,该成员可以是匿名
联合),则集合M(X)由X0和M(X0)的元素组成。
(3.7.3)如果X是联合类型,则集合M(X)是所有M(Ui)和包含所有Ui的集合的并集,其中每个Ui是X的第i个非静态数据成员的类型。
(3.7.4)如果X是一个元素类型为Xe的数组类型,则集合M(X)由Xe和M(Xe)的元素组成。
(3.7.5)如果X是非类,非数组类型,则集合M (X)为空。





评论


有人建议允许聚合具有基类,只要它是默认可构造的即可,请参见N4404

– Shafik Yaghmour
15年4月14日在14:49





#4 楼

C ++ 17的变化

在此处下载C ++ 17国际标准最终草案。

聚合

C ++ 17进行了扩展和增强聚合和聚合初始化。现在,标准库还包括std::is_aggregate类型的特征类。这是11.6.1.1和11.6.1.2节中的正式定义(省略了内部引用):


聚合是具有
的数组或类-无需用户提供,显式或继承的构造函数,
-没有私有或受保护的非静态数据成员,
-没有虚函数,并且
-没有虚拟,私有或受保护的基类。
[注意:聚合初始化不允许访问受保护的私有基类的成员或构造函数。 —尾注]
集合的元素为:
—对于数组,数组元素按下标顺序递增;或者对于
—对于类,按声明顺序为直接基类,后跟声明顺序的不是匿名联合的成员的直接非静态数据成员。


发生了什么变化?



聚合现在可以具有公共,非虚拟基类。此外,不要求基类是聚合。如果它们不是聚合,则将它们初始化为列表。

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
    &lt&lt "is C aggregate?: " &lt&lt (std::is_aggregate&ltC&gt::value ? 'Y' : 'N')
    &lt&lt " i1: " &lt&lt c.i1 &lt&lt " i2: " &lt&lt c.i2
    &lt&lt " j: " &lt&lt c.j &lt&lt " m.m: " &lt&lt c.m.m &lt&lt 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起的新定义(省略了内部引用):


一个可简单复制的类是一个类:
—每个复制构造函数,移动构造函数,复制赋值运算符,并且移动分配运算符被删除或变得微不足道,
—至少具有一个未删除的副本构造函数,move构造函数,副本分配运算符或move赋值运算符,并且
—具有一个琐碎的,未删除的析构函数。
一个琐碎的类是一个类它是可复制的,并具有一个或多个默认构造函数,所有这些构造函数都是平凡的或已删除,并且至少有一个未删除。 [注意:特别是,平凡可复制的
或平凡的类没有虚拟函数或虚拟基类。—尾注]


更改:


在C ++ 14下,对于一个琐碎的类,该类不能具有任何琐碎的复制/移动构造函数/赋值运算符。但是,隐式声明为默认的构造函数/运算符可能不是很简单,而是定义为已删除,因为例如,该类包含无法复制/移动的类类型的子对象。这种不平凡的,定义为删除的构造函数/运算符的存在将导致整个类不平凡。析构函数也存在类似的问题。 C ++ 17阐明了此类构造函数/运算符的存在不会导致该类不可平凡地复制,因此也不是平凡的,而一个平凡可复制的类必须具有一个平凡的,未删除的析构函数。 DR1734,DR1928

C ++ 14允许一个平凡的可复制类(因此是一个平凡的类)将每个复制/移动构造函数/赋值运算符声明为删除。如果此类也是标准布局,则可以使用std::memcpy对其进行合法复制/移动。这是一个语义上的矛盾,因为通过将所有构造函数/赋值运算符定义为“删除”,该类的创建者明确希望该类不能被复制/移动,但是该类仍满足普通可复制类的定义。因此,在C ++ 17中,我们有一个新的子句,规定琐碎可复制的类必须至少具有一个琐碎的,不可删除的(尽管不一定可以公共访问)复制/移动构造函数/赋值运算符。请参阅N4148,DR1734

第三项技术更改涉及默认构造函数的类似问题。在C ++ 14下,一个类可以具有琐碎的默认构造函数,这些构造函数隐式定义为Deleted,但仍然是一个普通的类。新定义阐明了一个琐碎的类必须具有至少一个琐碎的,未删除的默认构造函数。请参见DR1496


标准布局类

标准布局的定义也被重新设计以解决缺陷报告。同样,这些更改本质上是技术性的。这是来自标准(12.0.7)的文本。与以前一样,将省略内部引用:


如果类S是标准布局类,则它是:
—没有非标准数据类型的非静态数据成员布局类(或此类类型的数组)或引用,
—没有虚函数,也没有虚拟基类,
—对所有非静态数据成员具有相同的访问控制,
—没有非标准布局的基类,
—最多具有任何给定类型的基类子对象,
—该类及其基类中具有所有非静态数据成员和位字段首先在同一类中声明,并且
—不具有类型M(S)的类型(定义如下)的元素作为基类。108
M(X)定义如下:
—如果X是不包含(可能是继承的)非静态数据成员的非联合类类型,则集合M(X)为空。
—如果X是非联合类类型,其第一个非静态数据成员的类型为X0(其中该成员可以是匿名联合),集合M(X)由X0和M(X0)的元素组成。 > —如果X是联合类型,则集合M(X)是所有M(Ui)和包含所有Ui的集合的联合,其中每个Ui是X的第i个非静态数据成员的类型。 /> —如果X是元素类型为Xe的数组类型,则集合M(X)由Xe和M(Xe)的元素组成。
—如果X是非类,非数组类型,集合M(X)为空。
[注意:M(X)是所有非基类子对象的类型的集合,这些子对象在标准布局类中保证在处的零偏移量。 X. —结束注释]
[示例:
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)这样可确保两个具有相同类类型且属于t的子对象他最相同的派生对象不在同一地址分配。


更改:


阐明了要求派生中只有一个类树“具有”非静态数据成员是指首先声明此类数据成员的类,而不是可以继承它们的类,并将此要求扩展到非静态位字段。还阐明了标准布局类“最多具有任何给定类型的一个基类子对象”。参见DR1813,DR1881

标准布局的定义从未允许任何基类的类型与第一个非静态数据成员的类型相同。这是为了避免偏移量为零的数据成员具有与任何基类相同的类型的情况。 C ++ 17标准提供了更严格的递归定义,“禁止在标准布局类中保证所有零基础类的子对象的类型的偏移量为零”,以禁止此类类型成为任何基类的类型。请参阅DR1672,DR2120。


注意:尽管新语言不在发布的C +中,但C ++标准委员会打算根据缺陷报告对上述更改进行适用于C ++ 14。 +14标准。它符合C ++ 17标准。

评论


注意我刚刚更新了我的答案,标准布局更改缺陷具有CD4状态,这意味着它们实际上已应用于C ++ 14。这就是为什么我的答案没有包括他们的原因,这是我写完答案后发生的原因。

– Shafik Yaghmour
18/12/11在6:24

注意,我开始悬赏这个问题。

– Shafik Yaghmour
18/12/12在1:17

谢谢@ShafikYaghmour。我将检查缺陷报告的状态并相应地修改答案。

– ThomasMcLeod
18/12/12在16:20

@ShafikYaghmour,在对C ++ 14流程进行了一些回顾之后,在我看来,虽然这些DR在2014年6月的Rapperswil会议上被“接受”,但从2014年2月Issaquah会议开始使用的语言就是C ++ 14。请参见isocpp.org/blog/2014/07/trip-report-summer-iso-c-meeting,“根据ISO规则,我们尚未正式批准对C ++工作文件的任何修改。”我想念什么吗?

– ThomasMcLeod
18/12/18在19:50



它们具有“ CD4”状态,这意味着它们应在C ++ 14模式下应用。

– Shafik Yaghmour
18/12/24在2:50

#5 楼

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的move构造函数通过将其空置来修改move操作的源。这就是确保unique_ptr的值唯一的原因。琐碎的复制/移动操作意味着不会播放此类对象值的恶作剧,因此对象的值只能是其存储的二进制数据。


Trivial:此对象被认为具有它存储的任何位的功能值。普通可复制将对象的数据存储的含义定义为仅仅是该数据。但是这些类型仍然可以控制数据到达那里的方式(在某种程度上)。这样的类型可以具有默认成员初始化器和/或默认构造函数,以确保特定成员始终具有特定值。因此,可以将对象的概念值限制为它可以存储的二进制数据的子集。
在具有琐碎默认构造函数的类型上执行默认初始化将使该对象具有完全未初始化的值。因此,具有琐碎默认构造函数的类型在逻辑上对数据存储中的任何二进制数据都是有效的。


布局轴确实非常简单。决定如何将类的子对象存储在类的存储器中时,编译器有很多余地。但是,在某些情况下,没有必要留出余地,具有更严格的订购保证很有用。
这些类型是标准布局类型。而且,C ++标准实际上并没有特别说明布局的意义。它基本上说明了有关标准布局类型的三件事:


第一个子对象与对象本身的地址相同。


您可以使用offsetof获得从外部对象到其成员子对象之一的字节偏移。


union可以玩一些游戏,如果活动成员是通过并发成员的非活动成员来访问子对象的,正在(至少部分地)使用与正在访问的非活动布局相同的布局。


编译器通常允许标准布局对象映射到C中具有相同成员的struct类型。在C ++标准中没有声明;这正是编译器的感觉。
POD在这一点上基本上是一个无用的术语。它只是琐碎的可复制性(值仅是其二进制数据)和标准布局(其子对象的顺序定义得更好)的交集。可以从这种情况推断出类型类似于C,并且可以映射到相似的C对象。但是该标准对此没有说明。


请您详细说明以下规则:

我会尝试:

a)标准布局类必须使所有非静态数据成员具有相同的访问控制

这很简单:所有非静态数据成员都必须全部为publicprivateprotected。您不能同时使用publicprivate
它们的理由在于完全区分“标准布局”和“非标准布局”的理由。即,给予编译器自由选择如何将事物放入内存的自由。不只是关于vtable指针。
当他们在98年对C ++进行标准化时,他们必须基本预测人们将如何实现它。尽管他们在C ++的各种风格方面都有相当多的实现经验,但是他们对事情并不确定。因此,他们决定谨慎行事:给予编译器尽可能多的自由。这就是为什么C ++ 98中POD的定义如此严格的原因。它为大多数类的C ++编译器提供了很大的自由度。基本上,POD类型旨在作为特殊情况,这是您专门编写的原因。在C ++ 11上工作时,它们在编译器方面有很多经验。他们意识到... C ++编译器作者确实很懒。他们拥有所有这些自由,但他们对此却无能为力。
标准布局的规则或多或少地编纂了惯例:大多数编译器实际上根本不需要进行任何更改(如果有的话) (可能还有一些与相应的类型特征有关的东西)。
现在,当涉及到public / private时,情况有所不同。成员publicprivate的重新排序自由实际上对编译器很重要,尤其是在调试版本中。而且,由于标准布局的重点是与其他语言的兼容性,因此在调试与发行版本中不能使布局有所不同。
然后事实是,它并没有真正伤害用户。如果您要创建一个封装的类,那么很可能所有数据成员仍然是private。通常,您不会在完全封装的类型上公开公共数据成员。因此,对于只有少数想要这样做的用户来说,这是个问题。
所以这没有什么大的损失。

b)整个继承中只有一个类树可以具有非静态数据成员,

之所以如此,是因为它们再次标准化了标准布局:惯例。
对于拥有实际上存储事物的继承树的两个成员,没有通用的惯例。有些人将基类放在派生类之前,而另一些人则采用其他方式。如果成员来自两个基类,您将如何订购它们?等等。编译器在这些问题上分歧很大。
而且,由于零/一/无穷大规则,一旦您说可以拥有两个带有成员的类,就可以说任意多个。这就需要添加很多布局规则以处理此问题。您必须说出多重继承的工作原理,哪些类将其数据放在其他类之前,等等。这是很多规则,却很少获得实质性的收益。
您不能使所有不具有虚函数的东西都变成现实。默认的构造函数标准布局。

,第一个非静态数据成员不能属于基类类型(这可能会破坏别名规则)。

我真的不能跟这个说话。我对C ++的别名规则没有足够的了解,无法真正理解它。但这与基成员将与基类本身共享相同的地址有关。即:
struct Base {};
struct Derived : Base { Base b; };

Derived d;
static_cast<Base*>(&d) == &d.b;

这可能违反了C ++的别名规则。以某种方式。
但是,请考虑一下:具备此功能的能力实际上有多有用?由于只有一个类可以具有非静态数据成员,因此Derived必须是该类(因为它具有Base作为成员)。因此,Base必须为空(数据)。如果Base是空的,并且是基类,那么为什么还要有它的数据成员呢?
由于Base是空的,所以它没有状态。因此,任何非静态成员函数都将根据其参数而不是其this指针执行其操作。
因此,再次:没有太大的损失。

评论


感谢您的解释,它有很大帮助。尽管static_cast (&d)和&d.b是相同的Base *类型,但它们指向的是不同的东西,因此违反了别名规则。请纠正我。

–安德烈(Andriy Tylychko)
2012年2月29日在21:43

而且,为什么如果只有一个类可以具有非静态数据成员,那么派生对象必须是该类?

–安德烈(Andriy Tylychko)
2012年2月29日在21:44

@AndyT:为了使Derived的第一个成员成为其基类,它必须具有两件事:基类和成员。而且,由于层次结构中只有一个类可以具有成员(并且仍然是标准布局),因此这意味着其基类不能具有成员。

–尼科尔·波拉斯(Nicol Bolas)
2012年2月29日在22:04

@AndyT,是的,IME,关于别名规则,您基本上是正确的。要求具有相同类型的两个不同实例具有不同的内存地址。 (这允许使用内存地址跟踪对象身份。)基础对象和第一个派生成员是不同的实例,因此它们必须具有不同的地址,这迫使添加填充,从而影响类的布局。如果它们是不同类型,那就没关系了。允许不同类型的对象具有相同的地址(例如,一个类及其第一个数据成员)。

–亚当·彼得森(Adam H. Peterson)
16-2-22在22:28



#6 楼

在c ++ 20中发生了什么变化


继此问题的其余主题之后,聚合的含义和使用随每个标准而不断变化。有一些关键的变化即将出现。

具有用户声明的构造函数P1008的类型


在C ++ 17中,此类型仍是集合: br />
struct X {
    X() = delete;
};


因此,X{}仍然可以编译,因为那是聚合初始化-而不是构造函数调用。另请参见:私有构造函数何时不是私有构造函数?

在C ++ 20中,限制将从要求变为:


没有用户提供,explicit或继承的构造函数





没有用户声明或继承的构造函数


这已被C ++ 20工作草案所采用。这里的X或链接的问题中的C都不是C ++ 20中的集合。

这也可以产生溜溜球效果,例如以下示例:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};


在C ++ 11/14中,由于基类的原因,B不是聚合的,因此B{}执行值初始化,该值初始化在可访问的点调用B::B(),而A::A()调用B

在C ++ 17中,由于允许基类,所以B{}成为聚合,这使A进行了聚合初始化。这需要从{}拷贝列表初始化一个B,但是要从auto x = B();的上下文之外初始化它,这是无法访问的。在C ++ 17中,这是错误的格式(尽管B会很好)。

现在在C ++ 20中,由于上述规则更改,B再次不再是聚合(不是因为基类,而是因为用户声明的默认构造函数-即使它是默认的)。因此,我们返回到emplace()的构造函数,此代码段的格式变得正确。

从带括号的值列表中初始化聚合P960


出现的一个常见问题是希望将emplace -style构造函数与聚合一起使用:

struct X { int a, b; };
std::vector<X> xs;
xs.emplace_back(1, 2); // error


这是行不通的,因为X(1, 2)会尝试有效执行初始化X,这是无效的。典型的解决方案是在q4312079q上添加一个构造函数,但是有了这个建议(当前正在通过Core进行工作),聚合将有效地合成综合的构造函数,这些构造函数可以做正确的事情-并且行为类似于常规构造函数。上面的代码将按原样在C ++ 20中编译。

聚合P1021(特别是P1816)的类模板参数推导(CTAD)

在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中编译(不需要用户提供的推导指南)。

评论


尽管我会投票赞成,但添加它确实有点为时过早,但是我不知道有什么重大的改变会在C ++ 2x完成之前改变这一点。

– Shafik Yaghmour
18/12/17在16:58

@ShafikYaghmour是的,可能还为时过早。但是考虑到SD是新语言功能的最后期限,这是我所知道的仅有的两个正在进行中的操作-最坏的情况是我稍后将这些部分之一删除了吗?我只是看到悬而未决的问题,并认为这是在我忘记之前进行讨论的好时机。

–巴里
18/12/17在17:01

我了解,对于类似的案例,我曾被诱惑过几次。我总是担心一些主要的事情会改变,而我最终不得不重写它。

– Shafik Yaghmour
18/12/17在17:26

@ShafikYaghmour似乎这里什么都不会改变:)

–巴里
1月17日6:17

您应该在指定的初始值设定项上加上几句话

–user7769147
7月14日下午16:42