第四章 聚合扩展

C++中有一种初始化对象的方式叫做聚合初始化(aggregate initialization),它允许用花括号聚集多个值来初始化。

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

4.1 扩展聚合初始化的动机

如果没有这项特性的话,继承一个类之后就不能使用聚合初始化了,需要你为新类定义一个构造函数:

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}; // OK since C++17
MoreData y{"test1", 6.778, false}; // OK

注意,因为它现在是聚合体,其它初始化方式也是可以的:

MoreData u; // OOPS: value/done are uninitialized
MoreData z{}; // OK: value/done have values 0/false

如果这个看起来太危险了,你还是最好提供一个构造函数。

4.2 使用扩展的聚合初始化

关于这个特性的常见用法是列表初始化一个C风格的数据结构,该数据结构继承自一个类,然后添加了一些数据成员或者操作。比如:

struct Data {
  const char* name;
  double value;
};
struct PData : Data {
  bool critical;
  void print() const {
    std::cout << ✬[✬ << name << ✬,✬ << value << "]\n"; }
};

PData y{{"test1", 6.778}, false};
y.print();

这里里面的花括号会传递给基类Data的数据成员。

你可以跳过一些初始值。这种情况下这些元素是零值初始化(zero initalized)(调用默认构造函数或者将基本数据类型初始化为0,false或者nullptr)。比如:

PData a{};          // zero-initialize all elements
PData b{{"msg"}};   // same as {{"msg",0.0},false}
PData c{{}, true};  // same as {{nullptr,0.0},true}
PData d;            // values of fundamental types are unspecified

注意使用空的花括号和不使用花括号的区别。

  • a零值初始化所有成员,所以name被默认构造,double value被初始化为0.0,bool flag被初始化为false。
  • d只调用name的默认构造函数。所有其它的成员都没用被初始化,所以值是未指定的(unspecified)。

你也可以继承非聚合体来创建一个聚合体。比如:

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

甚至还可以继承多个非聚合体:

template<typename T>
struct D : std::string, std::complex<T>
{
  std::string data;
};

然后使用下面的代码初始化它们:

D<float> s{{"hello"}, {4.5,6.7}, "world"};        // OK since C++17
D<float> t{"hello", {4.5, 6.7}, "world"};         // OK since C++17
std::cout << s.data;                              // outputs: ”world”
std::cout << static_cast<std::string>(s);         // outputs: ”hello”
std::cout << static_cast<std::complex<float>>(s); // outputs: (4.5,6.7)

内部花括号的值(initializer_lists)会传递给基类,其传递顺序遵循基类声明的顺序。

这项新特性还有助于用很少的代码定义lambdas重载

4.3 聚合体定义

总结一下,C++17的聚合体(aggregate)定义如下:

  • 是个数组
  • 或者是个类类型(class,struct,union),其中
    • 没有用户声明的构造函数或者explicit构造函数
    • 没有使用using声明继承的构造函数
    • 没有private或者protected的非static数据成员
    • 没有virtual函数
    • 没有virtual,private或者protected基类

为了让聚合体可以使用,还要求聚合体没有private或者protected基类成员或者构造函数在初始化的时候使用。

C++17还引入了一种新的type trait即is_aggregate<>来检查一个类型是否是聚合体:

template<typename T>
struct D : std::string, std::complex<T> {
  std::string data;
};
D<float> s{{"hello"}, {4.5,6.7}, "world"}; // OK since C++17
std::cout << std::is_aggregate<decltype(s)>::value; // outputs: 1 (true)

4.4 向后不兼容

注意,下面示例中的代码将不再能通过编译:

// lang/aggr14.cpp
struct Derived;
struct Base {
  friend struct Derived;
private:
  Base() {
  }
};
struct Derived : Base {
};
int main()
{
  Derived d1{}; // ERROR since C++17
  Derived d2; // still OK (but might not initialize)
}

C++17之前,Derived不是一个聚合体,所以:

Derived d1{};

调用Derived隐式定义的默认构造函数,它默认调用基类Base的默认构造函数。虽然基类的默认构造函数是private,但是通过子类的默认构造函数调用它是有效的,因为子类被声明为一个friend类。

C++17开始,Derived是一个聚合体,没有隐式的默认构造函数。所以这个初始化被认为是聚合初始化,聚合初始化不允许调用基类的默认构造函数。不管基类是不是friend都不行。

4.5 后记

内联变量最初由Oleg Smolsky在https://wg21.link/n4404中提出。最后这个特性的公认措辞是由Oleg Smolsky在 https://wg21.link/p0017r1中给出。

新的type trait即std::is_aggregate<>最初作为美国国家机构对C++ 17标准化的评论而引入。(参见https://wg21.link/lwg2911