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

时间:2010-11-14 15:35:50

标签: c++ c++11 aggregate c++17 standard-layout

FAQ与聚合和POD有关,并涵盖以下内容:

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

6 个答案:

答案 0 :(得分:518)

答案 1 :(得分:422)

答案 2 :(得分:90)

C ++ 14的变化是什么

我们可以参考Draft C++14 standard作为参考。

聚集体

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

  

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

现在唯一的变化是添加类内成员初始值设定项不会使类成为非聚合类。以下示例来自C++11 aggregate initialization for classes with member in-pace initializers

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

不是C ++ 11中的聚合,但它是在C ++ 14中。 N3605: Member initializers and aggregates涵盖了此更改,其中包含以下摘要:

  

Bjarne Stroustrup和Richard Smith提出了一个关于聚合的问题   初始化和成员初始化程序不能一起工作。这个   本文提议通过采用史密斯提出的措辞来解决这个问题   这消除了聚合不能拥有的限制   构件-初始化。

POD保持不变

POD(普通旧数据)结构的定义在9 部分中有所说明:

  

POD结构 110 是一个非联合类,它既是一个普通的类,也是   标准布局类,没有类型的非静态数据成员   非POD结构,非POD联合(或此类类型的数组)。同样,a   POD联盟是一个既简单又简单的联盟   标准布局类,并且没有类型的非静态数据成员   非POD结构,非POD联合(或此类类型的数组)。一个POD类是   一个POD结构或POD联合的类。

与C ++ 11的措辞相同。

C ++ 14的标准布局更改

正如评论中所提到的, pod 依赖于标准布局的定义,而C ++ 14确实发生了变化,但这是通过应用于事后C ++ 14。

有三个DR:

所以standard-layout来自这个Pre C ++ 14:

  

标准布局类是一个类:

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

this in C++14

  

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

     
      
  • (3.1)没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
  •   
  • (3.2)没有虚函数,也没有虚基类,
  •   
  • (3.3)对所有非静态数据成员具有相同的访问控制,
  •   
  • (3.4)没有非标准布局基类
  •   
  • (3.5)最多具有任何给定类型的一个基类子对象,
  •   
  • (3.6)让类中的所有非静态数据成员和位字段及其基类首先在同一个类中声明,并且
  •   
  • (3.7)没有将类型M(S)的元素作为基类,其中对于任何类型X,M(X)定义如下。   [注意:M(X)是在X中可能处于零偏移的所有非基类子对象的类型的集合。    - 结束说明   ]      
        
    • (3.7.1)如果X是非联合类类型,没有(可能是继承的)非静态数据成员,则集合M(X)为空。
    •   
    • (3.7.2)如果X是非联合类类型,其中XO类型的非静态数据成员是零大小或者是第一个   X的非静态数据成员(其中所述成员可以是匿名的   union),集合M(X)由X0和M(X0)的元素组成。
    •   
    • (3.7.3)如果X是联合类型,则集合M(X)是所有M(Ui)和包含所有Ui的集合的并集,其中每个Ui是   ith的非静态数据成员。
    •   
    • (3.7.4)如果X是元素类型为Xe的数组类型,则集合M(X)由Xe和M(Xe)的元素组成。
    •   
    • (3.7.5)如果X是非类非数组类型,则集合M(X)为空。
    •   
  •   

答案 3 :(得分:43)

  

请您详细说明以下规则:

我会试试:

  

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

这很简单:所有非静态数据成员必须全部publicprivateprotected。您不能拥有一些public和一些private

他们的推理归结为完全区分“标准布局”和“非标准布局”的推理。也就是说,让编译器可以自由选择如何将内容放入内存中。这不仅仅是关于vtable指针。

当他们在98中标准化C ++时,他们必须基本预测人们将如何实现它。虽然他们有各种各样的C ++实现经验,但他们对事情并不确定。所以他们决定保持谨慎:给编译器尽可能多的自由。

这就是为什么C ++ 98中POD的定义如此严格。它使C ++编译器在大多数类的成员布局上具有很大的自由度。基本上,POD类型是特殊情况,特别是你出于某种原因写的。

当C ++ 11正在开发时,他们对编译器有了更多的经验。他们意识到...... C ++编译器编写者非常懒惰。他们拥有所有这些自由,但他们并没有任何事情。

标准布局的规则或多或少地编纂了常规做法:大多数编译器实际上并没有真正改变它们(对于相应的类型特征可能有些东西)。

现在,当谈到public / private时,事情就不同了。重新排序哪些成员publicprivate的自由实际上对编译器很重要,特别是在调试构建时。而且由于标准布局的重点是与其他语言兼容,因此在调试与发布之间不能使布局不同。

然后事实是它并没有真正伤害用户。如果你正在制作一个封装的类,那么你的所有数据成员都是private的几率很高。您通常不会在完全封装的类型上公开公共数据成员。因此,对于那些想要这样做的少数用户来说,这只会是一个问题,谁想要这种划分。

所以这不是什么大损失。

  

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

这个问题的原因可以追溯到为什么他们再次标准化标准布局:通常的做法。

当涉及到实际存储事物的继承树的两个成员时,存在 no 常见做法。有些人把基类放在派生之前,有些则用另一种方式做。如果成员来自两个基类,您会以哪种方式订购?等等。编制者在这些问题上存在很大差异。

另外,由于零/一/无限规则,一旦你说你可以有两个成员课程,你可以说你想要多少。这需要添加许多布局规则来处理这个问题。你必须说多重继承是如何工作的,哪些类把它们的数据放在其他类之前等等。这是很多规则,因为物质的获得很少。

您无法制作没有虚函数和默认构造函数标准布局的所有内容。

  

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

我真的不能对这个说话。我在C ++的别名规则方面没有受过足够的教育才能真正理解它。但它与基本成员将与基类本身共享相同地址这一事实有关。那就是:

struct Base {};
struct Derived : Base { Base b; };

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

这可能违反了C ++的别名规则。在某种程度上。

然而,考虑一下:实际实际是否有能力做到这一点?由于只有一个类可以拥有非静态数据成员,因此Derived必须是该类(因为它有Base作为成员)。因此Base 必须为空(数据)。如果Base为空,以及是基类......为什么要有数据成员?

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

再次:没有大的损失。

答案 4 :(得分:26)

C ++ 17中的变化

下载C ++ 17国际标准最终草案here

<强>聚集体

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

  

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

改变了什么?

  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
        << "is C aggregate?: " << (std::is_aggregate::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
    
    1. 不允许使用明确的默认构造函数
    2. struct D // not an aggregate
      {
          int i = 0;
          D() = default;
          explicit D(D const&) = default;
      };
      
      1. 不允许继承构造函数
      2. struct B1
        {
            int i1;
            B1() : i1(0) { }
        };
        struct C : B1 // not an aggregate
        {
            using B1::B1;
        };
        


        琐碎课程

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

          

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

        的变化:

        1. 在C ++ 14下,对于一个简单的类,该类不能拥有任何非平凡的复制/移动构造函数/赋值运算符。但是,隐式声明作为默认构造函数/运算符可能是非平凡的,但定义已删除,因为,例如,该类包含一个类类型的子对象,可以不被复制/移动。这种非平凡的,定义为删除的构造函数/运算符的存在将导致整个类是非平凡的。析构函数存在类似的问题。 C ++ 17阐明了这样的构造函数/运算符的存在并不会导致类非平凡地可复制,因此非平凡,并且一个简单的可复制类必须具有一个简单的,未删除的析构函数。 DR1734DR1928
        2. C ++ 14允许一个简单的可复制类,因此是一个简单的类,将每个复制/移动构造函数/赋值运算符声明为已删除。如果类也是标准布局,则可以使用std::memcpy合法地复制/移动它。这是一个语义上的矛盾,因为通过将所有构造函数/赋值运算符定义为已删除,该类的创建者明确表示该类无法复制/移动,但该类仍然满足了一个简单的可复制类的定义。因此,在C ++ 17中,我们有一个新的子句,声明平凡的可复制类必须至少有一个平凡的,未删除的(虽然不一定是公共可访问的)复制/移动构造函数/赋值运算符。请参阅N4148DR1734
        3. 第三个技术变更涉及默认构造函数的类似问题。在C ++ 14下,一个类可以有一些简单的默认构造函数,这些构造函数被隐式定义为已删除,但仍然是一个普通的类。新定义阐明了一个普通的类必须至少有一个简单的,未删除的默认构造函数。请参阅DR1496
        4. 标准布局类

          标准布局的定义也经过重新设计,以解决缺陷报告。这些变化再次是技术性的。这是标准(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)这确保了具有相同类类型且属于同一最大派生对象的两个子对象不在同一地址分配。

          的变化:

          1. 澄清要求只有派生树中的一个类&#34;具有&#34;非静态数据成员是指首先声明此类数据成员的类,而不是可以继承它们的类,并将此要求扩展到非静态位字段。还澄清了标准布局类&#34;最多具有任何给定类型的一个基类子对象。&#34;请参阅DR1813DR1881
          2. 标准布局的定义从未允许任何基类的类型与第一个非静态数据成员的类型相同。这是为了避免偏移零的数据成员与任何基类具有相同类型的情况。 C ++ 17标准提供了一个更严格的递归定义&#34;所有非基类子对象的类型集,它们在标准布局类中保证为零偏移&#34;以便禁止这些类型成为任何基类的类型。请参阅DR1672DR2120
          3. 注意: C ++标准委员会打算将基于缺陷报告的上述更改应用于C ++ 14,尽管新语言不在已发布的C ++ 14标准中。它符合C ++ 17标准。

答案 5 :(得分:6)

C ++ 20会发生什么变化

这还为时过早,因此将来某些答案可能会更改。遵循该问题的其余明确主题之后,聚集标准的含义和使用随每个标准而不断变化。有一些关键的变化即将出现。

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

的类型

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

struct X {
    X() = delete;
};

因此X{}仍会编译,因为这是聚合初始化-而不是构造函数调用。另请参阅:When is a private constructor not a private constructor?

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

  

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

  

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

它已被C++20 working draft中采用。这里的X和链接的问题中的C都不是C ++ 20中的聚合。

通过括号中的值P960

初始化聚合

出现的一个常见问题是要对集合使用emplace()样式的构造函数:

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

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

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

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

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

Point p{1, 2}; // error

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

template <typename T> Point(T, T) -> Point<T>;

但是,从某种意义上讲,这是一件“显而易见的事情”,并且基本上只是样板而已,因此语言将为您完成此任务。这项更改已在2018年11月得到Evolution的批准,因此上面的示例很可能会在C ++ 20中进行编译(不需要用户提供的推导指南)。