C++有很多初始化对象的方法。其中之一叫做 聚合体初始化(aggregate initialization) , 这是聚合体 专有的一种初始化方法。 从C语言引入的初始化方式是用花括号括起来的一组值来初始化类:
struct Data {
std::string name;
double value;
};
Data x = {"test1", 6.778};
自从C++11起,你可以忽略等号:
Data x{"test1", 6.778};
自从C++17起,聚合体可以拥有基类。也就是说像下面这种从其他类派生出的子类也可以使用这种初始化方法:
struct MoreData : Data {
bool done;
}
MoreData y{{"test1", 6.778}, false};
如你所见,聚合体初始化时可以用一个子聚合体初始化来初始化类中来自基类的成员。
另外,你甚至可以省略子聚合体初始化的花括号:
MoreData y{"test1", 6.778, false};
这样写将遵循嵌套聚合体初始化时的通用规则,你传递的实参被用来初始化哪一个成员取决于它们的顺序。
如果没有这个特性,那么所有的派生类都不能使用聚合体初始化,这意味着你要像下面这样定义构造函数:
struct Cpp14Data : Data {
bool done;
Cpp14Data (const std::string& s, double d, bool b) : Data{s, d}, done{b} {
}
};
Cpp14Data y{"test1", 6.778, false};
现在我们不再需要定义任何构造函数就可以做到这一点。 我们可以直接使用嵌套花括号的语法来实现初始化, 如果给出了内层初始化需要的所有值就可以省略内层的花括号:
MoreData x{{"test1", 6.778}, false}; // 自从C++17起OK
MoreData y{"test1", 6.778, false}; // OK
注意因为现在派生类也可以是聚合体,所以其他的一些初始化方法也可以使用:
MoreData u; // OOPS:value/done未初始化
MoreData z{}; // OK: value/done初始化为0/false
如果觉得这样很危险,可以使用成员初始值:
struct Data {
std::string name;
double value{0.0};
};
struct Cpp14Data : Data {
bool done{false};
};
或者,继续提供一个默认构造函数。
聚合体初始化的一个典型应用场景是对一个派生自C风格结构体并且添加了新成员的类进行初始化。例如:
struct Data {
const char* name;
double value;
};
struct CppData : Data {
bool critical;
void print() const {
std::cout << '[' << name << ',' << value << "]\n";
}
};
CppData y{{"test1", 6.778}, false};
y.print();
这里,内层花括号里的参数被传递给基类Data
。
注意你可以跳过初始化某些值。在这种情况下,跳过的成员将会进行默认初始化
(基础类型会被初始化为0
、false
或者nullptr
,类类型会默认构造)。
例如:
CppData x1{}; // 所有成员默认初始化为0值
CppData x2{{"msg"}} // 和{{"msg", 0.0}, false}等价
CppData x3{{}, true}; // 和{{nullptr, 0.0}, true}等价
CppData x4; // 成员的值未定义
注意使用空花括号和不使用花括号完全不同:
x1
的定义会把所有成员默认初始化为0值, 因此字符指针name
被初始化为nullptr
,double
类型的value
初始化为0.0
,bool
类型的flag
初始化为false
。x4
的定义没有初始化任何成员。所有成员的值都是未定义的。
你也可以从非聚合体派生出聚合体。例如:
struct MyString : std::string {
void print() const {
if (empty()) {
std::cout << "<undefined>\n";
}
else {
std::cout << c_str() << '\n';
}
}
};
MyString x{{"hello"}};
MyString y{"world"};
注意这不是通常的具有多态性的public
继承,因为std::string
没有虚成员函数,
你需要避免混淆这两种类型。
你甚至可以从多个基类和聚合体中派生出聚合体:
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
你可以像下面这样使用和初始化:
D<float> s{{"hello"}, {4.5, 6.7}, "world"}; // 自从C++17起OK
D<float> t{"hello", {4.5, 6.7}, "world"}; // 自从C++17起OK
std::cout << s.data; // 输出:"world"
std::cout << static_cast<std::string>(s); // 输出:"hello"
std::cout << static_cast<std::complex<float>>(s); //输出:(4.5,6.7)
内部嵌套的初值列表将按照继承时基类声明的顺序传递给基类。
这个新的特性也可以帮助我们用很少的代码定义重载的lambda
。
总的来说,在C++17中满足如下条件之一的对象被认为是 聚合体 :
-
是一个数组
-
或者是一个满足如下条件的 类类型 (
class
、struct
、union
): -
没有用户定义的和
explicit
的构造函数 -
没有使用
using
声明继承的构造函数 -
没有
private
和protected
的非静态数据成员 -
没有
virtual
函数 -
没有
virtual, private, protected
的基类
然而,要想使用聚合体初始化来 初始化 聚合体,那么还需要满足如下额外的约束:
- 基类中没有
private
或者protected
的成员 - 没有
private
或者protected
的构造函数
下一节就有一个因为不满足这些额外约束导致编译失败的例子。
C++17引入了一个新的类型特征is_aggregate<>
来测试一个类型是否是聚合体:
template<typename T>
struct D : std::string, std::complex<T> {
std::string data;
};
D<float> s{{"hello"}, {4.5, 6.7}, "world"}; // 自从C++17起OK
std::cout << std::is_aggregate<decltype(s)>::value; // 输出1(true)
注意下面的例子不能再通过编译:
struct Derived;
struct Base {
friend struct Derived;
private:
Base() {
}
};
struct Derived : Base {
};
int main()
{
Derived d1{}; // 自从C++17起ERROR
Derived d2; // 仍然OK(但可能不会初始化)
}
在C++17之前,Derived
不是聚合体。因此
Derived d1{};
会调用Derived
隐式定义的默认构造函数,这个构造函数会调用基类Base
的构造函数。
尽管基类的默认构造函数是private
的,但在派生类的构造函数里调用它也是有效的,
因为派生类被声明为友元类。
自从C++17起,例子中的Derived
是一个聚合体,所以它没有隐式的默认构造函数
(构造函数没有使用using
声明继承)。因此,d1
的初始化将是一个聚合体初始化,
如下表达式:
std::is_aggregate<Derived>::value
将返回true
。
然而,因为基类有一个private
的构造函数(见上一节)所以不能使用花括号来初始化。
这和派生类是否是基类的友元无关。