有一种初始化C++对象的方法是聚合初始化,它能初始化被大括号包括起来的多个值:
-
struct Data {
-
std::string name;
-
double value;
-
};
-
Data x{"test1", 6.778};
从C++17开始,聚合还可以有基类,因此可以初始化从其他类/结构体派生而来的结构体:
-
struct MoreData : Data {
-
bool done;
-
};
-
MoreData y{{"test1", 6.778}, false};
可以看到,聚合初始化现在支持以嵌套括号传递初始值给基类的成员变量。也可以省略嵌套括号:
-
MoreData y{"test1", 6.778, false};
一般要初始化嵌套聚合的规则是,将初始值按照基类/基结构体和成员的声明顺序进行传递。
注:所谓聚合,可以是数组,或简单的类C的类(即没有用户定义的构造函数,没有private和protected的非静态成员变量,没有虚函数,在C++17之前还要求不能有基类)。
扩展聚合初始化的动机
没有此特性的话,从一个禁止聚合初始化的类型派生结构体,需要定义一个构造函数:
-
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正确
-
MoreData y{"test1", 6.778, false}; // 正确
注意,这是因为它现在是个聚合了,其他初始化也可以:
-
MoreData u; // value和done没有初始化
-
MoreData z{}; // 可以,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值(调用缺省构造函数或用0,false或nullptr初始化基本数据类型)。例如:
-
CppData x1{}; // 以0初始化所有元素
-
CppData x2{{"msg"}}; // 等同于 {{"msg",0.0},false}
-
CppData x3{{}, true}; // 等同于 {{nullptr,0.0},true}
-
CppData x4; // 基本类型的值未指定
注意使用空大括号和不使用括号的区别:
-
x1
0初始化所有成员,因此string name
是缺省构造的,double value
是以0.0
初始化,bool flag
以false
初始化。 -
d
通过构造函数只初始化了string name
,所有其他成员变量都没有初始化,因此它们的值未定义。
可以从一个非聚合的类派生出聚合类。例如:
-
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起正确
-
D<float> t{"hello", {4.5, 6.7}, "world"}; // C++17起正确
-
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)
内部的初始化列表按基类的成员变量声明的顺序传到基类。
聚合的定义
总结一下,从C++17开始,聚合定义如下:
-
可以是一个数组
-
或是一自定义类型(类、结构体、或联合),但需要:
-
没有用户定义或显式的构造函数
-
没有通过using声明继承而来的构造函数
-
没有private或protected的非静态成员变量
-
没有虚函数
-
没有virtual,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开始正确
-
std::cout << std::is_aggregate<decltype(s)>::value; // 输出: 1 (true)
向后兼容
注意,以下代码将编译不过:
lang/aggr14.cpp
-
struct Derived;
-
struct Base {
-
friend struct Derived;
-
private:
-
Base() {
-
}
-
};
-
struct Derived : Base {
-
};
-
int main()
-
{
-
Derived d1{}; // C++17后不支持
-
Derived d2; // 仍然正常,但可能不能初始化
-
}
C++17以前, Derived
不是个聚合,所以
-
Derived d1{};
将调用隐含定义的 Derived
的缺省构造函数,它会再调用基类 Base
的缺省构造函数。尽管基类的构造函数是private的,但它可以被派生类的缺省构造函数调用,因为派生类被定义为friend类了。
C++17开始,示例代码中的 Derived
是一个聚合,不再有隐含的缺省构造函数(不再通过using声明继承构造函数)。所以通过聚合初始化进行初始化,但它不能调用private的基类构造函数。是不是基类的friend都没用。