带有基类的聚合类

1. C++17 中的聚合类

聚合类就是一种可以直接用初始化列表 {} 进行初始化的类(像 struct 或数组一样)。它通常用来表现“数据集合”,没有太复杂的封装逻辑。

在 C++17 之前,一个类要成为聚合类,必须满足以下严苛条件:

  1. 没有用户声明的构造函数
  2. 没有私有保护的非静态数据成员(必须是 public 的)。
  3. 没有虚函数
  4. 没有基类

在 C++17 之前,如果想让一个类继承自另一个类,同时又想保持聚合初始化的特性(即直接用 {} 赋值),是做不到的。一旦继承了基类,这个类就不再是聚合类了,必须写构造函数来转发参数给基类。

C++17 放宽了限制,允许聚合类拥有基类(前提是基类本身也是非虚的聚合类)。这意味着我们可以更方便地扩展数据结构。现在的规则是:

  1. 只要基类也是非虚的、没有私有/保护成员的,就可以继承它。
  2. 派生类依然可以不写构造函数,直接使用 {} 初始化。
  3. 初始化顺序: 先初始化基类部分,再初始化派生类成员。

综上所述,在C++17中对聚合类的要求如下:

  1. 必须是公有继承(private 或 protected 继承都不行)
  2. 不能是虚继承
  3. 不能有用户提供的构造函数
  4. 不能有私有或受保护的非静态数据成员,并且所有非静态数据成员必须是公有的
  5. 不能有虚函数

C++17 引入了关于复合类的类型萃取 std::is_aggregate,可以在编译时检查一个类型是否为聚合类。这对于写模板代码非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <type_traits>

struct Base
{
int x;
};
struct Derived : public Base
{
static int y;
};
struct NonDerived
{
int a;
NonDerived() {}
};

struct NonDerived1 : virtual public Base
{
int y;
};

struct NonDerived2 : public Base
{
//virtual void print(){}
private:
int num;

};

int main()
{
std::cout << std::boolalpha;
std::cout << "Base is aggregate: " << std::is_aggregate<Base>::value << std::endl; // true
std::cout << "Derived is aggregate: " << std::is_aggregate<Derived>::value << std::endl; // true (C++17)
std::cout << "NonDerived is aggregate: " << std::is_aggregate<NonDerived>::value << std::endl; // false
std::cout << "NonDerived1 is aggregate: " << std::is_aggregate<NonDerived1>::value << std::endl; // false
std::cout << "NonDerived2 is aggregate: " << std::is_aggregate<NonDerived2>::value << std::endl; // false

return 0;
}

2. 使用示例

2.1 聚合类初始化

在使用{}初始化聚合类对象的时候,初始化顺序为先初始化基类部分,再初始化派生类成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <string>
#include <vector>

// 简单的基类
struct Person
{
std::string name;
int age;
};

// 派生聚合类
struct Employee : Person
{
std::string department;
double salary;
int employee_id;
};

// 演示聚合初始化和使用
void demo_basic_aggregate()
{
std::cout << "=== 基本聚合类示例 ===" << std::endl;

// 聚合初始化
// 方式1: 从父类到子类, 按照数据成员顺序依次初始化
Employee emp{"张三", 30, "技术部", 15000.0, 1001};
// 方式2: 将父类数据写到 {} 作为一个整体, 它是子类数据的一部分
Employee emp{{"张三", 30}, "技术部", 15000.0, 1001};

// 访问成员
std::cout << "姓名: " << emp.name << std::endl;
std::cout << "年龄: " << emp.age << std::endl;
std::cout << "部门: " << emp.department << std::endl;
std::cout << "薪资: " << emp.salary << std::endl;
std::cout << "员工ID: " << emp.employee_id << std::endl;

std::cout << std::endl;
}

int main()
{
demo_basic_aggregate();
return 0;
}

2.2 结构化绑定

虽然在结构化绑定中直接解包带有基类的聚合比较少见,但聚合初始化为结构化绑定提供了底层支持。下面是C++17 中结构化绑定到数据成员模式的硬性规则:所有非静态 public 数据成员,必须全部来自同一个类:要么全部是派生类直接成员,要么全部来自同一个无歧义的 public 基类;不能一部分在基类(如果是多重继承也只能有一个基类中有数据成员)、一部分在派生类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <string>

// 简单的聚合类
struct RGBColor
{
int red;
int green;
int blue;
};

// 带有基类的聚合类
struct NamedColor : RGBColor
{
std::string name;
};


struct Alpha
{
static constexpr uint8_t alpha{255};
};
struct NameColor : public Alpha
{
string name;
int hex;
};

void demo_structured_binding()
{
std::cout << "=== 聚合类与结构化绑定 ===" << std::endl;

// 基本聚合类的结构化绑定
RGBColor color{255, 128, 64};
auto [r, g, b] = color;
std::cout << "RGB颜色: (" << r << ", " << g << ", " << b << ")" << std::endl;

// 带有基类的聚合类
NamedColor namedColor{{200, 100, 50}, "橙红色"};

// 手动解构派生类
std::cout << "命名颜色: " << namedColor.name
<< " RGB: (" << namedColor.red << ", "
<< namedColor.green << ", " << namedColor.blue << ")" << std::endl;

// 使用结构化绑定解构数据
NameColor c1{{}, "Blue", 0x0000ff};
auto[name, hex] = c1;

std::cout << std::endl;
}

int main()
{
demo_structured_binding();
return 0;
}

C++17 允许聚合类拥有基类,这是一个小但美的特性。它消除了继承和 POD(纯旧数据)类型之间的隔阂,让 C++ 代码在保持高性能的同时,写起来更加轻量和直观。