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

本常见问题解答关于聚合和POD,并涵盖以下内容:

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

  • 如何阅读:

    这篇文章相当长。 如果你想了解总量和POD(普通旧数据),请花点时间阅读。 如果您只对聚合体感兴趣,请阅读第一部分。 如果你只对POD感兴趣,那么你必须先阅读聚合的定义,含义和例子,然后你可以跳到POD,但我仍然建议阅读第一部分的全部内容。 聚合的概念对于定义POD是必不可少的。 如果你发现任何错误(甚至是轻微的,包括语法,文体学,格式,语法等),请留下评论,我会编辑。

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

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

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

    所以,好的,让我们来解析这个定义。 首先,任何数组都是一个聚合。 一个类也可以是一个聚合,如果......等等! 关于结构或工会什么都没有说,他们不可能是聚合? 是的他们可以。 在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)
    编译器会发出错误
    else (当n完全没有被指定,就像int a[] = {1, 2, 3};int a[] = {1, 2, 3};
    假设数组(n)的大小等于m,所以int a[] = {1, 2, 3}; 相当于int a[3] = {1, 2, 3};

    当标量类型(的目的boolintchardouble ,指针等)是值初始化这意味着它是与初始化0为该类型( falsebool0.0double等)。 当具有用户声明的默认构造函数的类类型的对象被初始化时,它的默认构造函数被调用。 如果默认构造函数是隐式定义的,那么所有非静态成员都是递归值初始化的。 这个定义不准确,有点不正确,但它应该给你基本的想法。 引用不能被初始化。 例如,如果类没有适当的默认构造函数,则非聚合类的值初始化可能会失败。

    数组初始化的例子:

    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}};
    

    在上面的例子中, yc初始化为'a'yxi110yxi220yi[0]20yi[1]30yf为初始值,即用0.0初始化。 受保护的静态成员d根本不会被初始化,因为它是static

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

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

    关于总量足够了。 现在我们可以定义一组更严格的类型,例如POD

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

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

    POD-struct是一个聚合类,它没有类型非POD-struct,non-POD-union(或这种类型的数组)或非引用的非静态数据成员,并且没有用户定义的复制赋值运算符,也没有用户定义的析构函数。 同样,POD-union是一个聚合联合,它没有类型非POD-struct,非POD-union(或这种类型的数组)或非引用的非静态数据成员,也没有用户定义的复制赋值运算符并没有用户定义的析构函数。 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
    };
    

    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
    
  • goto声明。 正如你所知道的,它是非法的(编译器应该发出一个错误),通过goto从某个变量尚未处于范围内的点跳转到已经处于范围内的点。 此限制仅适用于变量为非POD类型的情况。 在下面的例子中, f() g()是格式不正确的,而g()是格式良好的。 请注意,微软的编译器对这条规则太自由了 - 它只是在这两种情况下发出警告。

    int f()
    {
      struct NonPOD {NonPOD() {}};
      goto label;
      NonPOD x;
    label:
      return 0;
    }
    
    int g()
    {
      struct POD {int i; char c;};
      goto label;
      POD x;
    label:
      return 0;
    }
    
  • 保证在POD对象的开始处不会有填充。 换句话说,如果一个POD-class A的第一个成员是T类型的,那么你可以安全地从A*T* reinterpret_cast并获取指向第一个成员的指针,反之亦然。

  • 名单继续下去......

    结论

    了解POD究竟是什么是很重要的,因为如你所见,许多语言特征对他们的行为有所不同。


    C ++ 11有哪些变化?

    骨料

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

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

    好的,什么改变了?

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

    struct Aggregate {
        Aggregate() = default; // asks the compiler to generate the default implementation
    };
    

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

  • 现在,聚合不能为非静态数据成员设置任何括号或等于初始值设定项。 这是什么意思? 那么,这只是因为在这个新标准中,我们可以像这样直接在类中初始化成员:

    struct NotAggregate {
        int x = 5; // valid in C++11
        std::vector<int> s{1,2,3}; // also valid
    };
    

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

  • 所以,什么是一个聚合没有太大改变。 它仍然是一个基本的想法,适应新的功能。

    POD呢?

    POD经历了很多变化。 这个新标准放宽了很多关于POD的规则,标准中提供的定义方式发生了根本性的变化。

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

  • 它支持静态初始化,并且
  • 使用C ++编译POD会为您提供与在C编译的结构相同的内存布局。
  • 因此,这个定义被分成两个不同的概念:普通类和标准布局类,因为它们比POD更有用。 现在这个标准很少使用术语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
    };
    

    标准布局

    标准布局是第二个属性。 该标准提到这些对于与其他语言进行通信很有用,这是因为标准布局类具有相同的等效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
    

    结论

    有了这些新规则,现在可以有更多类型的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发生了什么变化

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

    骨料

    这在8.5.1节给出了以下定义:

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

    现在唯一的变化是添加类内成员初始化器不会使类成为非聚合类。 因此,C ++ 11的以下示例为具有成员步调初始值设定项的类聚合初始化:

    struct A
    {
      int a = 3;
      int b = 3;
    };
    

    在C ++ 11中不是聚合,而是在C ++ 14中。 此更改在N3605:成员初始值设定项和汇总项中进行了介绍,其中包含以下摘要:

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

    POD保持不变

    POD(普通旧数据)结构的定义在第9节中讲述:

    POD struct110是一个非联合类,它既是一个普通类又是一个标准布局类,并且没有类型非POD结构,非POD联合(或这种类型的数组)的非静态数据成员。 同样,POD联合是既是平凡类又是标准布局类的联合,并且没有类型非POD结构,非POD联合(或这种类型的数组)的非静态数据成员。 POD类是一个POD结构或POD结合的类。

    这与C ++ 11相同。

    链接地址: http://www.djcxy.com/p/6875.html

    上一篇: What are Aggregates and PODs and how/why are they special?

    下一篇: PODs, rvalue and lvalues