633

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

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

6 回答 6

654

如何阅读:

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

这个答案适用于 C++03。对于其他 C++ 标准,请参阅:

什么是聚合以及它们为何特别

C++ 标准 ( C++03 8.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
};

你明白了。现在让我们看看聚合有什么特别之处。与非聚合类不同,它们可以用花括号初始化{}。这种初始化语法通常用于数组,我们刚刚了解到这些是聚合。所以,让我们从他们开始。

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

if(m == n)
数组的第 i元素用 a i初始化
else if(m < n)
数组的前 m 个元素用 a 1 , a 2 , ..., a m和其他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, 指针等)的对象被值初始化时,这意味着它被初始化0为该类型(falsefor bool0.0fordouble等)。当具有用户声明的默认构造函数的类类型对象被值初始化时,它的默认构造函数被调用。如果默认构造函数是隐式定义的,那么所有非静态成员都会递归地进行值初始化。这个定义不精确而且有点不正确,但它应该给你基本的想法。不能对引用进行值初始化。例如,如果类没有适当的默认构造函数,则非聚合类的值初始化可能会失败。

数组初始化示例:

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.i1with 10, y.x.i2with 20, y.i[0]with 20, y.i[1]with初始化的,30并且y.f是值初始化的,也就是用 初始化的0.0。受保护的静态成员d根本没有初始化,因为它是static.

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

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

关于聚合体就足够了。现在我们可以定义一组更严格的类型,即 POD

什么是 POD 以及它们为何如此特别

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

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

哇,这个更难解析,不是吗?:) 让我们将工会排除在外(基于与上述相同的理由)并以更清晰的方式重新表述:

如果聚合类没有用户定义的复制赋值运算符和析构函数,并且其非静态成员都不是非 POD 类、非 POD 数组或引用,则该聚合类称为 POD。

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

  • 所有 POD 类都是聚合,或者,换句话说,如果一个类不是聚合,那么它肯定不是 POD
  • 类,就像结构一样,可以是 POD,即使这两种情况的标准术语都是 POD-struct
  • 就像在聚合的情况下一样,类具有哪些静态成员并不重要

例子:

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 类型的对象,没有这样的保证。此外,您可以使用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()格式正确。请注意,微软的编译器对这条规则过于宽松——它只是在两种情况下都发出警告。

     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*toT*获取指向第一个成员的指针,反之亦然。

名单还在继续……</p>

结论

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

于 2010-11-14T15:36:17.293 回答
492

C++11 有什么变化?

骨料

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

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

好的,有什么变化?

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

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

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

  2. 现在聚合不能有任何用于非静态数据成员的大括号或等号初始化器。这是什么意思?好吧,这只是因为有了这个新标准,我们可以像这样直接在类中初始化成员:

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

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

所以,什么是聚合并没有太大变化。它仍然是相同的基本思想,适应了新功能。

POD 呢?

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

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

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

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

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

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

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

琐碎的课程

Trivial是上面提到的第一个属性: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 条),

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

— 要么在最派生类中没有非静态数据成员,并且最多有一个具有非静态数据成员的基类,要么没有具有非静态数据成员的基类,并且

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

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

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

[注意:标准布局类对于与用其他编程语言编写的代码进行通信很有用。它们的布局在 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;
于 2011-08-25T11:48:47.087 回答
122

C++14 发生了什么变化

我们可以参考Draft 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: Member initializers and aggregates中涵盖了此更改,其中包含以下摘要:

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

POD 保持不变

POD( plain old data ) 结构的定义在9 Classes部分中介绍,它说:

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

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

C++14 的标准布局更改

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

有三个 DR:

所以标准布局来自这个 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) 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
  • (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) 为空。
于 2014-12-16T18:21:24.763 回答
65

C++17 的变化

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

骨料

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

聚合是一个数组或一个类,它
没有用户提供的、显式的或继承的构造函数,
没有私有或受保护的非静态数据成员,
没有虚函数,并且
没有虚拟、私有或受保护的基类。
[注意:聚合初始化不允许访问受保护和私有基类的成员或构造函数。—尾注]
聚合的元素是:
— 对于数组,数组元素按递增下标顺序排列,或
— 对于类,按声明顺序排列的直接基类,然后是非静态数据成员匿名工会的成员,按声明顺序。

发生了什么变化?

  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
  1. 不允许显式默认构造函数
struct D // not an aggregate
{
    int i = 0;
    D() = default;
    explicit D(D const&) = default;
};
  1. 不允许继承构造函数
struct B1
{
    int i1;
    B1() : i1(0) { }
};
struct C : B1 // not an aggregate
{
    using B1::B1;
};


琐碎的类

平凡类的定义在 C++17 中进行了重新设计,以解决 C++14 中未解决的几个缺陷。这些变化本质上是技术性的。这是 12.0.6 的新定义(省略了内部引用):

普通可复制类是这样的类:
— 其中每个复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符要么被删除,要么被删除
— 至少有一个未删除的复制构造函数、移动构造函数、复制赋值运算符,或移动赋值运算符,and
— 有一个普通的、未删除的析构函数。
平凡类是可以平凡复制并具有一个或多个默认构造函数的类,所有这些构造函数要么是平凡的,要么是已删除的,并且至少有一个没有被删除。[注意:特别是,平凡可复制或平凡的类没有虚函数或虚基类。-结束注释]

变化:

  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
—结束示例]
108) 这确保了具有相同类类型且属于相同最派生对象的两个子对象不会分配在相同地址。

变化:

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

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

于 2018-06-10T17:06:39.887 回答
52

C++11 中的 POD 在这里基本上分为两个不同的轴:琐碎性和布局。琐碎性是关于对象的概念值与其存储中的数据位之间的关系。布局是关于……嗯,一个对象的子对象的布局。只有类类型有布局,而所有类型都有微不足道的关系。

所以这就是琐碎性轴的含义:

  1. 不可复制:此类对象的值可能不仅仅是直接存储在对象中的二进制数据。

    例如,unique_ptr<T>存储一个T*; 这是对象内二进制数据的总和。但这不是a的全部unique_ptr<T>。Aunique_ptr<T>存储 anullptr或指向其生命周期由unique_ptr<T>实例管理的对象的指针。那管理是价值的一部分unique_ptr<T>。并且该值不是对象的二进制数据的一部分;它是由该对象的各种成员函数创建的。

    例如,分配nullptr给 aunique_ptr<T>不仅仅是更改对象中存储的位。这样的分配必须破坏unique_ptr. unique_ptr在不通过其成员函数的情况下操作 a 的内部存储会破坏该机制,在T*不破坏其当前管理的对象的情况下更改其内部存储将违反对象所具有的概念价值。

  2. Trivially copyable:此类对象的值完全是并且仅是其二进制存储的内容。这就是允许复制二进制存储等同于复制对象本身的原因。

    定义普通可复制性(普通析构函数、普通/删除复制/移动构造函数/赋值)的特定规则是仅二进制值类型所必需的。对象的析构函数可以参与定义对象的“值”,例如unique_ptr. 如果那个析构函数是微不足道的,那么它就不会参与定义对象的值。

    专门的复制/移动操作也可以参与对象的值。unique_ptr的移动构造函数通过将移动操作的源置为空来修改移动操作的源。这确保了 a 的值unique_ptr唯一的。微不足道的复制/移动操作意味着不会播放此类对象值恶作剧,因此对象的值只能是它存储的二进制数据。

  3. Trivial :这个对象被认为对它存储的任何位都有一个功能值。Trivially copyable 将对象的数据存储的含义定义为只是该数据。但是这些类型仍然可以控制数据如何到达那里(在某种程度上)。这种类型可以具有默认成员初始化器和/或默认构造函数,以确保特定成员始终具有特定值。因此,对象的概念值可以限制为它可以存储的二进制数据的子集。

    对具有普通默认构造函数的类型执行默认初始化将使该对象具有完全未初始化的值。因此,具有普通默认构造函数的类型在逻辑上对于其数据存储中的任何二进制数据都是有效的。

布局轴非常简单。编译器在决定类的子对象如何存储在类的存储中时有很大的余地。但是,在某些情况下,这种余地是不必要的,并且具有更严格的订购保证是有用的。

这些类型是标准布局类型。而 C++ 标准甚至并没有真正说明该布局是什么。它基本上说了关于标准布局类型的三件事:

  1. 第一个子对象与对象本身位于同一地址。

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

  3. union如果活动成员(至少部分)使用与被访问的非活动成员相同的布局,则可以通过联合的非活动成员访问子对象来玩一些游戏。

编译器通常允许标准布局对象映射到structC 中具有相同成员的类型。但 C++ 标准中没有声明;这正是编译器想要做的事情。

在这一点上, POD基本上是一个无用的术语。它只是琐碎可复制性(值只是其二进制数据)和标准布局(其子对象的顺序更明确)的交集。可以从这些事情中推断出该类型类似于 C 并且可以映射到类似的 C 对象。但该标准没有关于这种情况的声明。


您能否详细说明以下规则:

我会尽力:

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

这很简单:所有非静态数据成员必须是publicprivateprotected。你不能有 somepublic和 some 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;

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

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

由于Base是空的,它没有状态。this因此,任何非静态成员函数都将根据它们的参数而不是它们的指针来完成它们所做的事情。

再说一遍:没有大的损失。

于 2012-02-28T23:26:31.440 回答
30

有什么变化

在这个问题的其余明确主题之后,聚合的含义和使用随着每个标准而不断变化。有几个关键的变化即将到来。

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

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

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不是聚合,因此在可访问的点执行调用which calls的B{}值初始化。这是格式良好的。B::B()A::A()

在 C++17 中,B因为允许基类而成为聚合,这使得B{}聚合初始化。这需要复制列表初始化一个Afrom {},但从 的上下文之外B,它是不可访问的。在 C++17 中,这是格式错误的(auto x = B();虽然会很好)。

现在在 C++20 中,由于上述规则更改,B再次不再是聚合(不是因为基类,而是因为用户声明的默认构造函数——即使它是默认的)。所以我们回到了B's 的构造函数,这个片段变得格式良好。

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

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

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

这不起作用,因为emplace将尝试有效地执行初始化X(1, 2),这是无效的。典型的解决方案是向 中添加一个构造函数X,但是通过这个提议(目前正在通过 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 中编译(无需用户提供的演绎指南)。

于 2018-12-17T16:56:42.217 回答