什么是聚合和POD以及它们如何/为何特殊?

Arm*_*yan 525 c++ aggregate standard-layout c++11 c++17

常见问题解答涉及聚合和POD,并涵盖以下材料:

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

Arm*_*yan 541

如何阅读:

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

什么是聚合以及为什么它们是特殊的

C++标准的正式定义(C++ 038.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
};
Run Code Online (Sandbox Code Playgroud)

你明白了.现在让我们看看聚合是如何特殊的.与非聚合类不同,它们可以使用花括号进行初始化{}.这种初始化语法通常用于数组,我们只是了解到这些是聚合.那么,让我们从他们开始吧.

Type array_name[n] = {a1, a2, …, am};

if(m == n)数组的
i元素用 i
else初始化 if if(m <n)
数组的前m个元素用 1,a 2,..., m和其他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为该类型(false对于bool,0.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
}
Run Code Online (Sandbox Code Playgroud)

现在让我们看看如何使用大括号初始化聚合类.几乎是一样的.我们将按照它们在类定义中出现的顺序初始化非静态数据成员而不是数组元素(根据定义它们都是公共的).如果初始化程序少于成员,则其余部分进行值初始化.如果无法对未明确初始化的成员之一进行值初始化,则会出现编译时错误.如果有更多的初始值设定项,我们也会遇到编译时错误.

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}};
Run Code Online (Sandbox Code Playgroud)

在上述示例中y.c被初始化'a',y.x.i110,y.x.i220,y.i[0]20,y.i[1]30y.f为值初始化,即,与初始化0.0.受保护的静态成员d根本没有初始化,因为它是static.

聚合联合的不同之处在于您可以仅使用大括号初始化其第一个成员.我认为,如果你是使用C足够先进++,甚至考虑使用工会(它们的使用可能是非常危险的,必须仔细考虑的),你可以看看了规则工会的标准自己:).

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

关于聚合的足够多.现在我们可以定义更严格的类型集,即POD

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

C++标准的正式定义(C++ 039§4):

POD结构是一个聚合类,它没有非POD结构类型的非静态数据成员,非POD联合(或此类类型的数组)或引用,并且没有用户定义的副本赋值运算符,也没有用户定义的析构函数.类似地,POD-union是一个聚合联合,它没有非POD结构类型的非静态数据成员,非POD联合(或此类类型的数组)或引用,并且没有用户定义的副本赋值运算符并且没有用户定义的析构函数.POD类是POD结构或POD结合的类.

哇,这个解析起来比较困难,不是吗?:)让我们离开工会了(以同样的理由如上),并更清楚一点的方式改写:

如果聚合类没有用户定义的复制赋值运算符和析构函数,并且其非静态成员都不是非POD类,非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
};
Run Code Online (Sandbox Code Playgroud)

POD类,POD联合,标量类型和此类类型的数组统称为POD类型.
POD在很多方面都很特别.我只提供一些例子.

  • POD类最接近C结构.与它们不同,POD可以具有成员函数和任意静态成员,但这两者都不会改变对象的内存布局.因此,如果您想编写一个可以在C甚至.NET中使用的或多或少可移植的动态库,您应该尝试使所有导出的函数只接受POD类型的参数.

  • 非POD类类型的对象的生命周期在构造函数完成时开始,在析构函数完成时结束.对于POD类,生命周期在对象的存储被占用时开始,并在该存储被释放或重用时结束.

  • 对于POD类型的对象,标准保证当您memcpy将对象的内容放入char或unsigned char数组中,然后memcpy将内容返回到对象中时,该对象将保持其原始值.请注意,对于非POD类型的对象,不存在此类保证.此外,您可以安全地复制POD对象memcpy.以下示例假定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
    
    Run Code Online (Sandbox Code Playgroud)
  • 转到声明.您可能知道,从某个变量尚未在范围内的点到已经在范围内的点,通过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;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 保证在POD对象的开头不会有填充.换句话说,如果一个POD-A级的第一个成员是T型的,你可以放心地reinterpret_castA*T*和得到的指针第一个成员,反之亦然.

名单不断......

结论

重要的是要了解POD到底是什么,因为正如您所看到的,许多语言功能对它们的行为都不同.

  • @Kev:如果你设法将相同的信息打包成一个较短的答案,我们都会高兴地投票! (4认同)
  • 很好的答案.注释:"如果隐式定义默认构造函数,则递归地对所有非静态成员进行值初始化." 并且"例如,如果类没有适当的默认构造函数,则非聚合类的值初始化可能会失败." 不正确:使用implicitly-*声明*默认构造函数的类的值初始化不需要隐式定义的默认构造函数.因此,给定(插入`private:`视情况而定):`struct A {int const a; ``然后`A()`格式正确,即使`A`的默认构造函数定义不正确. (3认同)
  • @Armen也注意到你可以对同一个问题做多个答案.每个答案都可以包含问题解决方案的一部分.在我看来,拧上那个接受标记的东西:) (3认同)
  • 答案很棒.我有时会再次访问这篇文章.顺便说一句关于Visual Studio的警告.如上所述,pod的"goto语句"对MSVC编译器一无所知.但对于switch/case语句,它会生成编译错误.基于它的概念,我已经制作了一些测试pod-checker:http://stackoverflow.com/questions/12232766/test-for-pod-ness-in-c-c11/17141989#17141989 (3认同)
  • 在以"非POD类类型的对象的生命周期从构造函数完成时开始并在析构函数完成时结束"的项目符号开头.最后一部分应该说"当析构函数启动时". (2认同)

R. *_*des 438

C++ 11有什么变化?

骨料

聚合的标准定义略有改变,但它仍然几乎相同:

聚合是一个数组或类(第9节),没有用户提供的构造函数(12.1),非静态数据成员(9.2)没有大括号或等于初始值,没有私有或受保护的非静态数据成员(第11条),没有基类(第10条),没有虚函数(10.3).

好的,改变了什么?

  1. 以前,聚合可能没有用户声明的构造函数,但现在它不能拥有用户提供的构造函数.有区别吗?是的,有,因为现在你可以声明构造函数并默认它们:

    struct Aggregate {
        Aggregate() = default; // asks the compiler to generate the default implementation
    };
    
    Run Code Online (Sandbox Code Playgroud)

    这仍然是一个聚合,因为在第一个声明中默认的构造函数(或任何特殊成员函数)不是用户提供的.

  2. 现在,聚合不能为非静态数据成员提供任何大括号或相等的初始值.这是什么意思?嗯,这只是因为有了这个新标准,我们可以直接在类中初始化成员,如下所示:

    struct NotAggregate {
        int x = 5; // valid in C++11
        std::vector<int> s{1,2,3}; // also valid
    };
    
    Run Code Online (Sandbox Code Playgroud)

    使用此功能使该类不再是聚合,因为它基本上等同于提供您自己的默认构造函数.

那么,什么是聚合并没有太大变化.它仍然是相同的基本想法,适应新功能.

POD怎么样?

POD经历了很多变化.在这个新标准中放宽了许多关于POD的先前规则,并且标准中提供的定义方式发生了根本改变.

POD的想法是捕获基本上两个不同的属性:

  1. 它支持静态初始化,和
  2. 在C++中编译POD为您提供与在C中编译的结构相同的内存布局.

因此,定义被分为两个不同的概念:普通类和标准布局类,因为它们比POD更有用.该标准现在很少使用术语POD,更喜欢更具体的琐碎标准布局概念.

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

POD结构是一个非联合类,它既是普通类,也是标准布局类,并且没有非POD结构类型的非静态数据成员,非POD联合(或此类类型的数组).类似地,POD联合是一个简单类和标准布局类的联合,并且没有非POD结构类型的非静态数据成员,非POD联合(或这种类型的数组).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),和

- 选择复制/移动每个直接基类子对象的构造函数是微不足道的,并且

- 对于类类型(或其数组)的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
};
Run Code Online (Sandbox Code Playgroud)

标准布局

标准布局是第二个属性.标准提到这些对于与其他语言通信很有用,这是因为标准布局类具有与等效C结构或联合相同的内存布局.

这是另一个必须以递归方式保存成员和所有基类的属性.和往常一样,不允许使用虚函数或虚基类.这会使布局与C不兼容.

这里一个宽松的规则是标准布局类必须具有相同访问控制的所有非静态数据成员.以前这些必须是所有公众,但现在你可以让他们的私人或受保护的,只要它们是所有私人或全部保护.

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

这就是定义在标准文本中的用法:

标准布局类是一个类:

- 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,

- 没有虚函数(10.3),没有虚基类(10.1),

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

- 没有非标准布局基类,

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

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

标准布局结构是使用类键结构或类键类定义的标准布局类.

标准布局联合是使用类 - 键联合定义的标准布局类.

[ 注意:标准布局类对于与使用其他编程语言编写的代码进行通信非常有用.它们的布局在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
Run Code Online (Sandbox Code Playgroud)

结论

有了这些新规则,现在可以有更多类型的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;
Run Code Online (Sandbox Code Playgroud)

  • 可能希望为C++ 14更新这个,它删除了聚合的"无支撑或等于初始化器"要求. (5认同)
  • 您能否详细说明以下规则:a)标准布局类必须具有具有相同访问控制的所有非静态数据成员;b) 整个继承树中只有一个类可以有非静态数据成员,第一个非静态数据成员不能是基类类型(这可能会破坏别名规则)。特别是他们的原因是什么?对于后面的规则,你能提供一个打破混叠的例子吗? (2认同)
  • 关于别名:有一个 C++ 布局规则,如果类 C 有一个(空)基数 X,并且 C 的第一个数据成员是类型 X,那么第一个成员不能与基数 X 位于相同的偏移量;如果需要避免这种情况,它会在它前面获得一个虚拟填充字节。在同一地址有两个 X(或子类)的实例可能会破坏需要通过它们的地址区分不同实例的事情(空实例没有其他东西可以区分它......)。在任何情况下,需要放入填充字节都会破坏“布局兼容”。 (2认同)

Sha*_*our 99

C++ 14改变了什么

我们可以参考Draft C++ 14标准以供参考.

骨料

8.5.1 聚合部分对此进行了介绍,它给出了以下定义:

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

唯一的变化是现在添加类内成员初始值设定项不会使类成为非聚合.因此,以下来自C++ 11的示例聚合了具有成员in-pace initializers的类的初始化:

struct A
{
  int a = 3;
  int b = 3;
};
Run Code Online (Sandbox Code Playgroud)

它不是C++ 11中的聚合,而是C++ 14中的聚合.N3605中包含此更改:成员初始值设定项和聚合,具有以下摘要:

Bjarne Stroustrup和Richard Smith提出了一个关于聚合初始化和成员初始化器无法协同工作的问题.本文提出通过采用史密斯提出的措辞来解决这个问题,该措辞消除了聚合不能拥有成员初始化者的限制.

POD保持不变

POD(普通旧数据)结构的定义在9 类Classes中介绍,它们说:

POD结构110是非联合类,它既是普通类又是标准布局类,并且没有非POD结构类型的非静态数据成员,非POD联合(或这种类型的数组).类似地,POD联合是一个简单的类和标准布局类的联合,并且没有非POD结构类型的非静态数据成员,非POD联合(或这种类型的数组).POD类是POD结构或POD联合的类.

这与C++ 11的措辞相同.

C++的标准布局更改14

正如评论中所指出的,pod依赖于标准布局的定义,并且对于C++ 14确实有所改变,但这是通过在事实之后应用于C++ 14的缺陷报告实现的.

有三个DR:

所以标准布局来自这个Pre C++ 14:

标准布局类是一个类:

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

为了在C++ 14:

类S是标准布局类,如果它:

  • (3.1)没有非标准布局类(或这类类型的数组)或引用的非静态数据成员,
  • (3.2)没有虚函数,也没有虚基类,
  • (3.3)对所有非静态数据成员具有相同的访问控制,
  • (3.4)没有非标准布局基类,
  • (3.5)最多有一个任何给定类型的基类子对象,
  • (3.6)该类中的所有非静态数据成员和位字段及其基类首先在同一个类中声明,并且
  • (3.7)没有集合M(S)类型的元素作为基类,其中对于任何类型X,M(X)定义如下.104 [注意:M(X)是类型的集合在X中可能处于零偏移的所有非基类子对象. - 结束注释]
    • (3.7.1)如果X是非联合类类型,没有(可能是继承的)非静态数据成员,则集合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](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4404. HTML) (4认同)

Nic*_*las 44

你能详细说明以下规则:

我会尽力:

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

这很简单:所有的非静态数据成员必须全部public,privateprotected.你不能拥有一些public和一些private.

对它们的推理归结为完全区分"标准布局"和"非标准布局"的原因.也就是说,让编译器可以自由选择如何将内容放入内存中.这不仅仅是关于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;
Run Code Online (Sandbox Code Playgroud)

这可能是针对C++的别名规则.某种程度上来说.

但是,请考虑一下:实际上有能力做到这一点有多大用处?由于只有一个类可以拥有非静态数据成员,因此Derived必须是该类(因为它有Base一个成员).所以Base 必须是空的(数据).如果Base是空的,还有一个基类......为什么要有一个数据成员呢?

由于Base是空的,它没有状态.因此,任何非静态成员函数都将根据它们的参数执行它们的操作,而不是它们的this指针.

再说一次:没有大的损失.

  • @AndyT:为了让'Derived`的第一个成员成为它的基类,它必须有两个东西:基类和*成员*.并且由于层次结构中只有一个类可以拥有成员(并且仍然是标准布局),这意味着它的基类不能拥有成员. (3认同)
  • @AndyT,是的,关于别名规则,你基本上是正确的,IME.需要两个相同类型的不同实例才能具有不同的内存地址.(这允许使用内存地址跟踪对象标识.)基础对象和第一个派生成员是不同的实例,因此它们必须具有不同的地址,这会强制添加填充,从而影响类的布局.如果它们属于不同的类型,那就无所谓了; 允许具有不同类型的对象具有相同的地址(例如,类及其第一个数据成员). (2认同)

Tho*_*eod 36

C++的变化17

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

骨料

C++ 17扩展并增强了聚合和聚合初始化.标准库现在还包括一个std::is_aggregate类型特征类.以下是第11.6.1.1节和第11.6.1.2节中的正式定义(内部参考文献已省略):

聚合是一个数组或类
- 没有用户提供的,显式的或继承的构造函数,
- 没有私有或受保护的非静态数据成员,
- 没有虚函数,
- 没有虚拟,私有或受保护的基类.
[注意:聚合初始化不允许访问受保护和私有基类的成员或构造函数.-end note]
聚合的元素是:
- 对于数组,数组元素按增加的下标顺序,或
- 对于类,按声明顺序的直接基类,后跟非直接的非静态数据成员声明顺序中的匿名工会成员.

改变了什么?

  1. 聚合现在可以拥有公共的非虚拟基类.此外,并不要求基类是聚合.如果它们不是聚合,则列表初始化.
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
Run Code Online (Sandbox Code Playgroud)
  1. 不允许使用明确的默认构造函数
struct D // not an aggregate
{
    int i = 0;
    D() = default;
    explicit D(D const&) = default;
};
Run Code Online (Sandbox Code Playgroud)
  1. 不允许继承构造函数
struct B1
{
    int i1;
    B1() : i1(0) { }
};
struct C : B1 // not an aggregate
{
    using B1::B1;
};
Run Code Online (Sandbox Code Playgroud)


琐碎的课程

在C++ 17中重新设计了普通类的定义,以解决C++ 14中未解决的几个缺陷.这些变化属于技术性质.这是12.0.6的新定义(内部参考文献省略):

一个简单的可复制类是一个类:
- 其中每个复制构造函数,移动构造函数,复制赋值运算符和移动赋值运算符被删除或无关紧要,
- 至少有一个未删除的复制构造函数,移动构造函数,复制赋值运算符,或者移动赋值运算符,并且
- 具有一个简单的,未删除的析构函数.
一个普通的类是一个可以轻易复制的类,它有一个或多个默认的构造函数,所有这些都是微不足道的或删除的,并且至少有一个不被删除.[注意:特别是,一个简单的可复制或普通的类没有虚函数或虚基类.-结束语]

变化:

  1. 在C++ 14下,对于一个简单的类,该类不能具有任何非平凡的复制/移动构造函数/赋值运算符.但是,隐式声明为默认的构造函数/运算符可能是非平凡的,但仍定义为已删除,因为例如,类包含无法复制/移动的类类型的子对象.这种非平凡的,定义为删除的构造函数/运算符的存在将导致整个类是非平凡的.析构函数存在类似的问题.C++ 17阐明了这样的构造函数/运算符的存在并不会导致类非平凡地可复制,因此非平凡,并且一个简单的可复制类必须具有一个简单的,未删除的析构函数.DR1734,DR1928
  2. C++ 14允许一个简单的可复制类,因此是一个普通的类,将每个复制/移动构造函数/赋值运算符声明为已删除.如果类也是标准布局,则可以合法地复制/移动std::memcpy.这是一个语义上的矛盾,因为通过将所有构造函数/赋值运算符定义为已删除,该类的创建者明确表示该类无法复制/移动,但该类仍然满足了一个简单的可复制类的定义.因此,在C++ 17中,我们有一个新的子句,声明平凡的可复制类必须至少有一个平凡的,未删除的(虽然不一定是公共可访问的)复制/移动构造函数/赋值运算符.见N4148,DR1734
  3. 第三个技术变化涉及默认构造函数的类似问题.在C++ 14下,一个类可以有一些简单的默认构造函数,这些构造函数被隐式定义为已删除,但仍然是一个普通的类.新定义阐明了一个普通的类必须至少有一个简单的,未删除的默认构造函数.见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
Run Code Online (Sandbox Code Playgroud) -end example]
108)这确保了具有相同类类型且属于同一最派生对象的两个子对象不在同一地址分配.

变化:

  1. 阐明了派生树中只有一个类"拥有"非静态数据成员的要求是指首先声明此类数据成员的类,而不是可以继承它们的类,并将此要求扩展到非静态位域.还澄清了标准布局类"最多只有一个任何给定类型的基类子对象." 参见DR1813,DR1881
  2. 标准布局的定义从未允许任何基类的类型与第一个非静态数据成员的类型相同.这是为了避免偏移零的数据成员与任何基类具有相同类型的情况.C++ 17标准提供了一个更严格的递归定义,即"在标准布局类中保证零偏移的所有非基类子对象的类型集",以便禁止这些类型从是任何基类的类型.参见DR1672,DR2120.

注意: C++标准委员会打算将基于缺陷报告的上述更改应用于C++ 14,尽管新语言不在已发布的C++ 14标准中.它符合C++ 17标准.


Bar*_*rry 9

什么会改变C++ 20

现在还为时尚早,所以有些答案可能会在未来发生变化.按照这个问题的其他明确主题,聚合的含义和用法随着每个标准而不断变化.目前有几个关键的变化.

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

在C++ 17中,这种类型仍然是聚合:

struct X {
    X() = delete;
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

这已被C++ 20工作草案采用.X这里和C链接的问题都不是C++ 20中的聚合.

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

出现的一个常见问题是希望使用B带有聚合的-style构造函数:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};
Run Code Online (Sandbox Code Playgroud)

这不起作用,因为B{}会尝试有效地执行初始化B::B(),这是无效的.典型的解决方案是添加一个构造函数A::A(),但是通过这个提议(目前通过Core工作),聚合将有效地具有合成的构造函数,它们做正确的事情 - 并且表现得像常规构造函数.上面的代码将在C++ 20中按原样编译(假设此功能获得批准,这似乎很可能).

聚合物P1021的类模板参数推导(CTAD)

在C++ 17中,这不编译:

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

用户必须为所有聚合模板编写自己的演绎指南:

template <typename T>
struct Point {
    T x, y;
};

Point p{1, 2}; // error
Run Code Online (Sandbox Code Playgroud)

但由于这在某种意义上是"显而易见的事情",而且基本上只是样板,语言会为你做这件事.这个变化在2018年11月被Evolution批准,所以上面的例子很可能在C++ 20中编译(不需要用户提供的演绎指南).

  • 您应该在**指定的初始值设定项**上添加几句话 (4认同)

归档时间:

查看次数:

134953 次

最近记录:

6 年,4 月 前