什么是聚合和pod,它们有什么特别之处?

这个常见问题解答是关于聚合体和PODs的,涵盖了以下材料:

  • 什么是骨料< em > < / em >?
  • 什么是# eyz0(普通旧数据)?
  • 它们之间有什么关系?
  • 他们有什么特别之处?
  • c++ 11有什么变化?
189115 次浏览

阅读方法:

这篇文章相当长。如果你想了解聚合和PODs(普通旧数据),花点时间阅读它。如果您只对聚合感兴趣,请只阅读第一部分。如果你只对PODs感兴趣,那么你必须首先阅读定义、含义和聚合的例子,然后你五月跳到PODs,但我仍然建议阅读第一部分的全文。聚合的概念对于定义pod至关重要。如果你发现任何错误(即使很小,包括语法,文体,格式,语法等)请留下评论,我会编辑。

这个答案适用于c++ 03。有关其他c++标准,请参阅:

什么是聚合?为什么它们是特殊的

# EYZ0:

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

好,我们来解析一下这个定义。首先,任何数组都是一个聚合。一个类也可以是一个聚合,如果……等等!没有提到结构体或并集,难道它们不能是集合吗?是的,他们可以。在c++中,术语class指的是所有的类、结构和联合。因此,当且仅当一个类(或结构或联合)满足上述定义中的条件时,它才是一个聚合。这些标准意味着什么?

  • 这并不意味着聚合类不能有构造函数,事实上它可以有默认构造函数和/或复制构造函数,只要它们是由编译器隐式声明的,而不是由用户显式声明的

  • 没有private或protected 非静态数据成员。您可以拥有任意数量的私有和受保护的成员函数(但不包括构造函数),以及任意数量的私有或受保护的< >强静态< / >强数据成员和成员函数,并且不违反聚合类的规则

  • 聚合类可以有用户声明的/用户定义的复制赋值操作符和/或析构函数

  • 一个数组是一个聚合,即使它是非聚合类类型的数组。

现在我们来看一些例子:

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

你懂的。现在让我们看看聚合的特殊之处。与非聚合类不同,它们可以用花括号{}进行初始化。这种初始化语法通常用于数组,我们刚刚了解到这些是聚合。让我们从它们开始。

# EYZ0

< p > # EYZ0
数组的ith元素初始化为
# EYZ0
数组的前m个元素初始化为12,…,,其他n - m元素,如果可能的话,是初始化值(参见下面的术语解释)
# EYZ0
编译器将发出一个错误
# EYZ1 # EYZ2 < BR > 假设数组的大小(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}};

在上面的例子中,y.c'a'初始化,y.x.i110初始化,y.x.i220初始化,y.i[0]20初始化,y.i[1]30初始化,'a'0用'a'1初始化。受保护的静态成员'a'2根本没有初始化,因为它是'a'3。

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

现在我们知道了聚合的特殊之处,让我们试着理解类的限制;这就是他们在那里的原因。我们应该理解,用大括号按成员初始化意味着类只不过是其成员的和。如果存在用户定义的构造函数,则意味着用户需要做一些额外的工作来初始化成员,因此大括号初始化是不正确的。如果存在虚函数,这意味着该类的对象(在大多数实现中)有一个指向类的所谓虚表的指针,该指针在构造函数中设置,因此大括号初始化是不够的。你可以用类似的方法计算出其余的限制:)。

关于聚集体讲够了。现在我们可以定义一组更严格的类型,即pod

什么是pod ?为什么它们很特别

# EYZ0:

POD-struct是一个聚合类 的非静态数据成员 type non-POD-struct, non-POD-union(或 数组)或引用,以及 没有用户定义的副本分配 操作符,没有自定义的 析构函数。类似地,POD-union是 一个没有 类型的非静态数据成员 non-POD-struct, non-POD-union(或 数组)或引用,以及 没有用户定义的副本分配 操作符,没有自定义的 析构函数。POD类是一个类 要么是pod结构体,要么是 POD-union . < / p >

哇,这个更难理解了,不是吗?:)让我们把工会排除在外(基于与上面相同的理由),用更清楚的方式重新措辞:

如果一个聚合类被称为POD 它没有用户定义的拷贝赋值 运算符和析构函数都没有 它的非静态成员是非pod 类、非pod数组或 参考。< / p >

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

  • 所有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-classes、pod -union、标量类型和此类类型的数组统称为< >强POD-types。< / >强
pod在很多方面都很特别。我将提供一些例子

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

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

  • 对于POD类型的对象,当您将对象的内容memcpy转换为char或unsigned 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
    
  • < p > 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-class a的第一个成员是类型T,您可以安全地从A*T*获取指向第一个成员的指针,反之亦然。

这样的例子不胜枚举……

结论

理解POD到底是什么很重要,因为正如您所看到的,许多语言特性对它们的行为是不同的。

C + + 11有什么变化?

总量

总量的标准定义略有改变,但仍然大致相同:

聚合是没有用户提供的构造函数(12.1)的数组或类(子句9) , 非静态数据成员没有 大括号或等于初始值设定项(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 的想法基本上是捕获两个截然不同的属性:

  1. 它支持静态初始化,并且
  2. 在 C + + 中编译 POD 可以得到与在 C 中编译的结构相同的内存布局。

因此,定义被分成两个不同的概念: 微不足道类和 标准布局类,因为它们比 POD 更有用。该标准现在很少使用术语 POD,倾向于更具体的 微不足道标准布局概念。

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

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

让我们分别详细讨论这两个属性中的每一个。

琐碎的课程

琐碎的 是上面提到的第一个属性: 琐碎的类支持静态初始化。 如果一个类是可复制的(一个普通类的超集) ,那么可以将它的表示形式复制到类似于 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 结构或 类键类。

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

[ 注:标准布局类对于与用其他编程语言编写的代码进行通信非常有用。它们的布局在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 + + 11中的 POD 基本上分为两个不同的轴: 平凡性和布局。琐碎性是关于对象的概念价值和其存储中的数据位之间的关系。布局是关于... 对象子对象的布局。只有类类型有布局,而所有类型都有琐碎的关系。

因此,这就是平凡轴的意义所在:

  1. 非平凡的可复制 : 这种类型的对象的值可能不仅仅是直接存储在对象中的二进制数据。

    例如,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*而不破坏它目前管理的对象,将违反对象所拥有的概念价值。

  2. 琐碎的可复制 : 这些对象的值正是且仅是它们的二进制存储的内容。这就是为什么允许复制二进制存储等效于复制对象本身是合理的。

    定义简单复制性的特定规则(简单的析构函数、简单的/删除的复制/移动构造函数/赋值)是类型成为只有二进制值的类型所需要的。对象的析构函数可以参与定义对象的“值”,如 unique_ptr的情况。如果那个析构函数是微不足道的,那么它就不会参与定义对象的值。

    专门的复制/移动操作也可以参与到对象的值中。unique_ptr的 move 构造函数修改 move 操作的源,使其为 null。这就确保了 unique_ptr的值为 独一无二。微不足道的复制/移动操作意味着这样的对象值骗局不会被玩弄,因此对象的值只能是它存储的二进制数据。

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

    对一个缺省构造函数很小的类型执行默认初始化会使该对象的值完全未初始化。因此,对于数据存储中的任何二进制数据,一个缺省构造函数很小的类型在逻辑上是有效的。

布局轴确实很简单。编译器在决定如何将类的子对象存储在类的存储器中时有很大的余地。然而,在某些情况下,这种回旋余地是不必要的,具有更严格的顺序保证是有用的。

这种类型是 标准布局类型标准布局类型。而且 C + + 标准甚至没有明确说明布局是什么。关于标准布局类型,它基本上说明了三件事:

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

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

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

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

此时,POD 基本上是一个无用的术语。它只是平凡的可复制性(值只是它的二进制数据)和标准布局(它的子对象的顺序更加明确)的交集。人们可以从这些事情中推断出,该类型是类 C 的,并且可以映射到类似的 C 对象。但是标准并没有说明这一点。


请详细说明以下规则:

我试试:

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

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

他们的理由来自于区分“标准布局”和“非标准布局”的理由。也就是说,给编译器自由选择如何将东西存入内存。这不仅仅是可视指针的问题。

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

这就是 C + + 98中 POD 的定义如此严格的原因。它给了 C + + 编译器在大多数类的成员布局上很大的自由度。基本上,POD 类型是为了特殊情况而设计的,您专门编写这些类型是有原因的。

在开发 C + + 11时,他们对编译器有更多的经验。他们意识到... C + + 编译器的编写者真的很懒。他们拥有所有的自由,但他们没有 任何与之相关的东西。

标准布局的规则或多或少地编纂了一些常见的实践: 大多数编译器实际上并不需要做太多的修改,即使有的话也可以实现它们(也许除了一些相应类型特征的内容之外)。

现在,当谈到 public/private时,情况就不同了。对于编译器来说,重新排列哪些成员是 public还是 private的自由实际上很重要,特别是在调试构建中。由于标准布局的重点在于与其他语言的兼容性,因此在调试和发布时,布局不能有所不同。

然后还有一个事实,那就是它并没有真正伤害到使用者。如果您正在创建一个封装的类,那么您的所有数据成员很可能都是 private。通常不会在完全封装的类型上公开公共数据成员。所以这只是那些想要这么做的少数用户的问题,他们想要这个部门。

所以没什么损失。

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

这个问题的原因又回到了他们为什么要标准化布局的原因: 普遍的实践。

当涉及到一个实际存储事物的继承树的两个成员时,没有有一个常见的实践。有些将基类置于派生类之前,有些则以另一种方式。如果成员来自两个基础类,您会用哪种方式排序?诸如此类。编译器在这些问题上分歧很大。

另外,感谢0/1/无穷大规则,一旦你说你可以有两个带成员的类,你可以想说多少就说多少。这需要添加大量的布局规则来处理这个问题。你必须说明多重继承是如何工作的,哪些类把他们的数据放在其他类之前,等等。这是一个很大的规则,为很少的物质利益。

没有虚拟功能和缺省构造函数标准布局的东西是无法制造出来的。

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

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

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


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

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

然而,考虑一下这个问题: 拥有这样做的能力有多大用处?因为只有一个类可以有非静态数据成员,所以 Derived必须是那个类(因为它有一个 Base作为成员)。因此 Base 必须的是空的(数据)。如果 Base是空的,那么 还有就是一个基类... 为什么还要有一个它的数据成员呢?

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

再说一遍,没什么损失。

C + + 14发生了什么变化

我们可以参考 C + + 14标准草案作为参考。

总量

这在 8.5.1 总量节中有所涉及,该节给出了以下定义:

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

现在唯一的改变是添加 类内成员初始值设定项并不使一个类成为非聚合类:

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

不是 C + + 11中的聚合,而是 C + + 14中的聚合。N3605: 成员初始化器和聚合涵盖了这一变化,其摘要如下:

比雅尼·斯特劳斯特鲁普和理查德•史密斯提出了一个关于总量的问题 初始化和成员初始化不能一起工作 文件建议通过采用史密斯提议的措辞来解决这个问题 消除聚合不能具有的限制 成员初始化器成员初始化器。

POD 保持不变

POD (普通的旧数据)结构的定义包含在 9 课程部分中,该部分说:

POD struct110是一个非联合类,它既是一个普通类,又是一个 标准布局类,并且没有类型为 非 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)没有与第一个非静态数据成员相同类型的基类

在 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 是一个非联合类类型,其非静态数据成员类型为 X0,大小为零或者是第一个 X 的非静态数据成员(其中该成员可能是匿名的 联合) ,集合 M (X)由 X0和 M (X0)的元素组成。
    • (3.7.3)如果 X 是并集类型,则集合 M (X)是所有 M (UI)的并集和包含所有 UI 的集合,其中每个 UI 是 X 的非静态数据成员。
    • (3.7.4)如果 X 是元素类型为 Xe 的数组类型,则集合 M (X)由 Xe 和元素 M (Xe)组成。
    • (3.7.5)如果 X 是非类、非数组类型,则集合 M (X)为空。

C + + 17的变化

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

总量

C + + 17扩展并增强聚合和聚合初始化。标准库现在还包括一个 std::is_aggregate类型的 trait 类。以下是第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中的新定义(省略了内部参考文献) :

一个微不足道的可复制类是一个类:
ー其中每个复制建构子、 move 构造函数、 copy 赋值运算符和 move 赋值运算符或者被删除或者不重要,
ーー其中至少有一个未删除的复制建构子、 move 构造函数、 copy 赋值运算符或 move 赋值运算符,以及
它具有一个不重要的、不删除的析构函数。
一个普通类是一个普通可复制的类,它有一个或多个默认构造函数,所有这些构造函数要么是普通的,要么是已删除的,并且至少有一个构造函数没有被删除。[注意: 特别是一个可以轻易复制的 或普通类不具有虚函数或虚基类ーー结束注释]

变化:

  1. 在 C + + 14中,对于一个非平凡的类,该类不能有任何非平凡的复制/移动构造函数/赋值操作符。然而,作为默认构造函数/运算符的隐式 声明可能是非平凡的,而作为删除的 定义可能是非平凡的,因为,例如,类包含一个类类型的子对象,不能被复制/移动。这种非平凡的、定义为删除的构造函数/运算符的存在将导致整个类非平凡。析构函数也存在类似的问题。C + + 17澄清了这样的构造函数/运算符的存在并不会导致类是非平凡的可复制类,因此是非平凡的,并且一个非平凡的可复制类必须有一个平凡的、不删除的析构函数。DR1734DR1928
  2. C + + 14允许一个普通的可复制类(因此是一个普通类)将每个复制/移动构造函数/赋值操作符声明为删除。如果这样的类也是标准的布局,但是,它可以合法复制/移动与 std::memcpy。这是一个语义上的矛盾,因为,通过定义为删除所有构造函数/赋值操作符,类的创建者显然意图类不能被复制/移动,但类仍然符合一个微不足道的可复制类的定义。因此,在 C + + 17中,我们有了一个新的子句,声明可复制类必须至少有一个不重要的、不删除的(尽管不一定是公开可访问的)拷贝/移动构造函数/赋值操作符。参见 N4148DR1734
  3. 第三个技术变化与默认构造函数的类似问题有关。在 C + + 14中,一个类可以有一些微不足道的默认构造函数,这些构造函数被隐式定义为已删除,但仍然是一个微不足道的类。新的定义澄清了一个普通类必须至少有一个普通的、未删除的缺省构造函数。参见 DR1496

标准版面类别

标准布局的定义也被重新定义以处理缺陷报告。这些变化同样是技术性的。下面是来自标准(12.0.7)的文本。与以前一样,内部参考文献被省略:

类 S 是一个标准布局类,如果它:
ー没有类型非标准布局类(或此类型的数组)或引用的非静态数据成员,
ー没有虚函数和虚基类,
ー对所有非静态数据成员具有相同的访问控制,
ー没有非标准布局的基类,
ー最多只有一个给定类型的基类子对象,
ー拥有类中的所有非静态数据成员和位字段及其首先在同一个类中声明的基类,以及
ー没有类型集 M (S)的元素(下面定义)作为基类
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)这样可以确保两个具有相同类型且属于相同的大多数派生对象的子对象不会分配到相同的地址。< br >

变化:

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

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

的变化

继这个问题的其余明确主题之后,总量的含义和使用继续随着每个标准而变化。有几个关键的变化即将出现。

具有用户声明的构造函数 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没有的聚合,因此 B{}执行值初始化,该初始化调用 B::B()B::B()调用 A::A(),在可访问的位置。这是精心制作的。

在 C + + 17中,由于允许基类,B变成了一个聚合,从而实现了 B{}的聚合初始化。这需要从 {}复制列表初始化一个 A,但是从 B的上下文之外进行初始化,在 B的上下文之外无法访问 A。在 C + + 17中,这是格式不正确的(但是 auto x = B();可以)。

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

从括号中的值 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(特别是 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中编译(不需要用户提供的演绎指南)。