Aggregate类型以及值初始化

时间:2021-06-20 13:33:36

引言

在C++中,POD是一个极其重要的概念。要理解POD类型,我们首先需要理解Aggregate类型。下文结合*上的高票回答将对Aggregate类型做一个全面的解读。

对于Aggragates的定义

C++标准(C++ 03 8.5.1 §1)中的正式定义如下:

An aggregate is an array or a class (clause 9) with no user-declared constructors (12.1), no private or protected non-static data members (clause 11), no base classes (clause 10), and no virtual functions (10.3).

译文:一个Aggregate是一个数组或者一个没有用户声明构造函数,没有私有或保护类型的非静态数据成员,没有父类和虚函数的类型

从定义中我们首先可以解读出以下两点信息:

1. 数组一定是Aggregate。

2. 满足以下定义的class也可以是Aggregate(注意:class包括classes、structs和unions):

-> Aggregate不能拥有用户自定义的构造函数。事实上,它可以拥有一个默认构造函数或者一个默认复制构造函数,只要它们是被编译器声明,而非用户自定义的即可。

-> Aggregate不能拥有private或者protected的非静态数据成员。事实上,你可以定义其他private和protected成员方法(不包括构造函数,对于Aggregate,不能由用户自定义构造函数,参见上条),也可以定义private和protected静态类型的数据成员和方法,这都不会违背aggregate类型的规则。

-> Aggregate没有父类和虚函数。

-> Aggregate类型可以由用户自定义赋值操作符和析构函数。

注意:数组一定是Aggregate类型,即便数组中存放的是非Aggregate类型的元素。

例子

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

Why Aggregates are special?

与非Aggregate类型不同的是,Aggregate类型可以使用“{}”来进行初始化。这种初始化语法,实际上在数组中是很常见的。别忘了我们的数组类型也属于Aggregate,来看以下例子:

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

1. 如果m与n相等:很自然,数组的第i个元素被初始化为ai。

2. 如果m小于n   :数组的前m个元素被依次初始化为a1,a2,…,am,剩余的n-m个元素将进行值初始化(前提是可以进行值初始化)。

3. 如果m大于n   :产生编译错误。

注意,类似于以下形式也是正确的:

int a[] = {1,2,3};) // 等价于 int a[3] = {1,2,3};

数组的长度将由编译器推测为3,因此以上两种形式是等价的。

值初始化(value-initialization

先来看以下解释,摘自*:

When an object of scalar type (bool, int, char, double, pointers, etc.) is value-initialized it means it is initialized with 0 for that type (false for bool, 0.0 for double, etc.). When an object of class type with a user-declared default constructor is value-initialized its default constructor is called. If the default constructor is implicitly defined then all nonstatic members are recursively value-initialized. This definition is imprecise and a bit incorrect but it should give you the basic idea. A reference cannot be value-initialized. Value-initialization for a non-aggregate class can fail if, for example, the class has no appropriate default constructor.

译文如下:

对于标量类型(如:bool、int、char、double、指针)的对象如果按值初始化,是指其将被初始化为0(如:bool类型将被初始化为false,double类型将被初始化为0.0)。

默认构造函数由用户自定义的类类型对象如果按值初始化,那么其默认构造函数将会被调用;如果默认构造函数为隐式定义,那么所有的非静态数据成员将会递归地按值初始化。

虽然以上定义并不精确,也不完全,但是可以让我们有个基本的认识。注意,引用不能按值初始化。对于非Aggregate类型的class进行值初始化,可能会失败,例如没有合适的默认构函数。

1. 来看以下数组初始化的例子:

class A()
{
A(int){} // 用户自定义了构造函数,因此是非Aggrerate类型。注意,类A没有默认构造函数。定义对象时,无法进行值初始化
};
class B()
{
B() {} // 用户自定义了构造函数,因此是非Aggrerate类型。注意,类B拥有可用的默认构造函数。定义对象时,可以进行值初始化。
}; int main()
{
A a1[3] = {A(2), A(1), A(14)}; // OK n == m
A a2[3] = {A(2)}; // ERROR A没有默认构造函数. 不能按值初始化a2[1] 和 a2[2]
B b1[3] = {B()}; // OK b1[1]和b1[2]使用默认构造函数按值初始化 int Array1[1000] = {0}; // 所有元素被初始化为0
int Array2[1000] = {1}; // 注意: 只有第一个元素被初始化为1,其他为0;
bool Array3[1000] = {}; // 大括号里可以为空,所有元素被初始化为false; int Array4[1000]; // 没有被初始化. 这和空{}初始化不同;这种情形下的元素没有按值初始化,他们的值是未知的,不确定的; (除非Array4是全局数据) int array[2] = {1,2,3,4}; // ERROR, 太多初始值,编译出错。
}

2. 现在我们来看Aggregates类类型是如何使用{ }进行初始化的。和对数组进行初始化非常类似,按照在类内部声明的顺序(按照定义都必须是public类型)初始化非静态类型的成员变量。如果初始值比成员少,那么其他的成员将按值初始化。如果有一个成员无法进行按值初始化,我们将会得到一个编译期错误。如果初始值比成员多,我们同样得到一个编译期错误。

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.i1被初始化为10,y.x.i2被初始化为20,y.i[0]为20,y.i[1]为30,y.f被按值初始化为0.0。protected类型的static数据成员d不会被初始化,因为它是静态类型的。 

值初始化与默认初始化的区别

对于内置类型变量而言,值初始化就是zero-initialization,默认初始化将根据变量所在位置而有所区别。以下讨论针对类类型。

在C++03标准中,针对类类型,只要用户没有自定义构造函数,那么就认为默认构造函数为隐式定义(即使其中有const类型变量,有引用类型)。

1. 如果用户自定义(显式定义)默认构造函数,那么无论值初始化还是默认初始化,都将会调用默认构造函数。

2. 如果默认构造函数为隐式定义,那么:

1)对于默认初始化,其采取的策略是分别对类成员递归地进行默认初始化,那么类中不能出现const内置类型成员、没有显式定义默认构造函数的const类类型对象、引用类型。

2)对于值初始化,其采取的策略是分别对类成员递归地进行值初始化,那么类中可以出现const类型成员,但是仍然不能出现引用类型成员(这是很显然的,因为引用不能按值初始化)。

注意:针对c++03标准,对于非静态数据成员,是不能给类内初始值的。c++11可以。

总结如下:

1. 默认初始化:内置类型变量在函数体内值未定义,在函数体外值为0。类类型变量调用默认构造函数进行默认初始化,显式默认构造函数直接调用,隐式默认构造函数则递归对其成员进行默认初始化。

2. 值初始化:内置类型变量为zero-initialization。类类型变量调用默认构造函数进行默认初始化,显式默认构造函数直接调用,隐式默认构造函数则递归对其成员进行值初始化。

ps:所有const成员都需要用户显式初始化。因此const内置类型成员无法默认初始化以及没有显式定义默认构造函数的const类类型对象无法默认初始化,而由用户显式定义了默认构造函数的const类类型对象可以完成默认初始化。而对于值初始化,实际上属于显式初始化,因为值初始化本质上是用零去初始化(针对内置类型变量)和用用户自定义的默认构造函数去初始化(针对类类型对象)。默认初始化,本质上是用一个未定义值去初始化(针对内置类型变量,属于隐式初始化)和用用户自定义的默认构造函数去初始化(针对类类型对象,属于显式初始化)。

pps:对于引用类型,既不能默认初始化,也不能按值初始化。

请看以下例子:

#include <iostream>
using namespace std; class A
{
public:
const int ival;
}; int main()
{
A object = {}; // 如果写成A object;进行默认初始化,由于A没有显式定义默认构造函数,因此对其成员递归进行默认初始化,而const内置类型变量必须用户显式初始化,程序报错
         cout << object.ival << endl;
}

调用该程序时,对象object 进行值初始化,由于该类默认构造函数为隐式定义,因此其成员a会递归地进行值初始化,为0。

再看以下例子:

#include <iostream>
#include <string>
#include <vector>
using namespace std; class A
{
public:
int ival;
//A():ival(0){}
void print() const
{
cout << ival << endl;
}
}; class B
{
public:
const A object_;
void print()
{
cout << "right" << endl;
}
}; int main(int argc, const char *argv[])
{
B object;
// object.object_.print();
object.print();
return 0;
}

main函数中对象object进行默认初始化。由于B没有显示定义默认构造函数,因此递归对其成员A(常量类类型对象)进行默认初始化,而A也没有显式定义默认构造函数,因此对常量对象A 默认初始化失败,程序报错。

总结

我们知道了Aggregates的特别之处,现在让我们来尝试理解一下它对类型的限制,也就是说为什么会有这些限制。来看以下解释,同样摘自*:

We should understand that memberwise initialization with braces implies that the class is nothing more than the sum of its members. If a user-defined constructor is present, it means that the user needs to do some extra work to initialize the members therefore brace initialization would be incorrect. If virtual functions are present, it means that the objects of this class have (on most implementations) a pointer to the so-called vtable of the class, which is set in the constructor, so brace-initialization would be insufficient. You could figure out the rest of the restrictions in a similar manner as an exercise :)

我们应当理解使用{ }对成员进行逐一初始化意味着这一类型仅仅是成员的集合。

如果有一个用户定义的构造函数,那意味着用户需要做一些额外的工作来初始化成员,因此使用{ }初始化是不正确的。如果出现了虚函数,那意味着这个类型(大多数实现)有一个指向vtable的指针,需要在构造函数内设置,所以使用{ }初始化是不够的。我们可以按照这种方式理解其他限制的含义。

综上,Aggregare类型可以使用{ }进行初始化,且Aggregare类类型一定可以进行值初始化(前提是类中没有引用类型成员)。

之后的博文,我将会通过Aggregate类型来介绍POD(原生数据类型)。

参考文献

1. What are Aggregates and PODs and how/why are they special?