11.5 Unions

11.5.1 General

1
一个联合体是一个使用[class-key]():`union`定义的类
2
在一个联合体中,如果某个非静态数据成员的名字指向一个生命周期已经开始且尚未结束([[basic.life]]())的对象,则该非静态数据成员处于活动状态。在任何时刻,一个联合体类型的对象内至多只能有一个非静态数据成员处于活动状态,也就是说,在同一时刻,一个联合体中至多只能存储一个非静态数据成员的值。
【注1:为了简化联合体的使用,提供了一项特殊保证:如果一个标准布局的联合体包含若干个共享相同初始序列([[class.mem]]())的标准布局结构体,并且该标准布局联合体类型对象的某个非静态数据成员处于活动状态,且该活跃成员是这些标准布局结构体之一,那么这些标准布局结构体成员中的任意一个的公共初始序列都可以被检查(`inspected`);参见([[class.mem]]())。——注释结束】
3
联合体的大小足以容纳其最大的非静态数据成员。每个非静态数据成员的分配方式,就像它是一个非联合类中唯一的成员一样。
【注2:联合体对象与其非静态数据成员是指针可互换的(`pointer-interconvertible`)([[basic.compound]]()、[[expr.static.cast]]())。因此,联合体对象的所有非静态数据成员具有相同的地址。——注释结束】
4

联合体可以拥有成员函数(包括构造函数和析构函数),但不能拥有虚函数([class.virtual])。联合体不能有基类,也不能被用作基类。若联合体包含引用类型的非静态数据成员,则程序是非法(ill-formed)的。

【注3:如果联合体包含具有非平凡的拷贝构造函数和(或)移动构造函数([class.copy.ctor])和(或)拷贝赋值运算符和(或)移动赋值运算符([class.copy.assign])的非静态数据成员,则该联合体的对应成员函数必须由用户提供,否则它将会被隐式地删除([dcl.fct.def.delete])。

【示例1:

1
2
3
4
5
union U {
int i;
float f;
std::string s;
};

由于std::string[string.classes])声明了所有特殊成员函数的非平凡版本,因此U的拷贝/移动构造函数以及拷贝/移动赋值运算符将会被隐式删除。尽管std::string具有非平凡的默认构造函数和非平凡的析构函数,但U的默认构造函数和析构函数却都是平凡的。——示例结束】

——注释结束】

Paw5zx个人理解:点击查看更多

联合体在默认构造时不会初始化任何成员,因为它没有一个默认活动成员;同样联合体不会析构任何成员,因为它无法知道当前处于活动状态的成员是哪一个。所以编译器在为U生成默认构造函数和默认析构函数时,不会调用s的构造和析构函数,所以U的默认构造函数和析构函数都是平凡的。

但是在gcc中,似乎会直接将默认构造函数和析构函数标记为deleted需要用户手动实现,否则无法通过编译

5
当赋值运算符的左操作数(`left operand`)涉及到一个指向联合体成员的成员访问表达式([[expr.ref]]())时,它可能会按如下所述,开始该联合体成员的生命周期。对于一个表达式`E`,定义`E`的子表达式集合`S(E)`如下:
(5.1)
- 如果`E`的形式是`A.B`,则`S(E)`包含`S(A)`中的元素。并且还包含`A.B`本身,前提是`B`命名的是以下类型之一的联合体成员: - 非类、非数组类型,或 - 具有非删除的平凡默认构造函数的类类型,或 - 上述类型的数组。
(5.2)
- 如果`E`的形式是`A[B]` 并且被解释为内置(`built-in`)的数组下标运算符,则: - 若`A`是数组类型,则`S(E)`为`S(A)`; - 若`B`是数组类型,则`S(E)`为`S(B)`; - 否则`S(E)`为空。
(5.3)
- 否则,`S(E)`为空。
在一个形式为`E1 = E2`的赋值表达式中,如果该表达式使用的是内置赋值运算符([[expr.assign]]())或平凡的赋值运算符([[class.copy.assign]]()),则对于`S(E1)`中的每个元素`X`以及每个属于联合体成员且以该元素作为直接子对象(递归地)的匿名联合体成员`X`([[class.union.anon]]()),如果修改`X`会根据[[basic.life]]()导致未定义行为,那么将在所指名的存储中隐式创建一个`X`类型的对象;不会执行任何初始化,并且其生命周期的开始将被排序在左、右操作数的值计算之后、赋值操作之前。
【注4:这会结束联合体中先前处于活动状态的成员的生命周期(如果有的话)([[basic.life]]())。——注释结束】
【示例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
union A { int x; int y[4]; };
struct B { A a; };
union C { B b; int k; };
int f() {
C c; // 不会开始任何联合体成员的生命周期
c.b.a.y[3] = 4; // OK, S(c.b.a.y[3])包含c.b和c.b.a.y;
// 创建对象来持有联合体成员c.b和c.b.a.y
return c.b.a.y[3]; // OK, c.b.a.y指向新创建的对象
}

struct X { const int a; int b; };
union Y { X x; int k; };
void g() {
Y y = { { 1, 2 } }; // OK, y.x是联合体的活动成员
int n = y.x.a;
y.k = 4; // OK, 结束了y.x的生命周期, y.k现在是联合体的活动成员
y.x.b = n; // 未定义行为: y.x.b在其生命周期外被修改,
// 由于X的默认构造函数被删除(因为const int a没有初始化),S(y.x.b)为空
// 因此联合体成员y.x的生命周期不会隐式开始
}
——示例结束】
Paw5zx个人理解:点击查看更多

如果赋值表达式的左操作数是对联合体某个成员的访问,那么这个赋值操作可能会隐式启动该成员的生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
union U {
int i;
struct Plain { int x; } p;
std::string s;
// 数组情况略
};

int main()
{
U u; // 不开启任何成员的生命周期
u.i = 3; // S(u.i) = {u.i},u.i被隐式激活
u.p = {23}; // S(u.p) = {u.p},结束u.i生命周期,u.p被隐式激活
u.s = "Hello"; // 左右操作数求值:计算u.s的地址,在计算右值"Hello"
// 编译器分析:S(u.s) = {u.s},并判断直接修改u.s会导致未定义行为
// 结束u.p生命周期
// 编译器会在u.s对应的存储空间隐式创建一个类型为std::string的一个对象(u.s生命周期开始),类似new (&u.s) std::string,但不会调用构造函数;
// 然后赋值
}
6
【注5:在上述规则不适用的情况下,联合体的活动成员只能通过使用[new表达式]()来更改。——注释结束】
【示例3:考虑一个联合体类型`U`的对象`u`,联合体`U`具有非静态数据成员`m`(类型为`M`)和`n`(类型为`N`)。 如果`M`具有非平凡的析构函数,而`N`具有非平凡的构造函数(例如,它们声明或继承了虚函数),则可以通过以下方式,利用析构函数和[new表达式](),安全地将`u`的活动成员从`m`切换为`n`:
1
2
u.m.~M();
new (&u.n) N;
——示例结束】

11.5.2 Anonymous unions

1
形如:
1
union { member-specification } ;
的联合体被称为匿名联合体(`anonymous union`);它定义了一个未命名的类型和一个该类型的未命名对象(如果它是非静态数据成员则称为匿名联合体成员,否则称为匿名联合体变量)。在匿名联合体的[member-specification]()中的每个[成员声明]()必须要么定义一个或多个`public`非静态数据成员,要么是一个[静态断言声明]()。嵌套类型、匿名联合体和函数不得在匿名联合体中声明。匿名联合体成员的名字绑定在该联合体声明所在的作用域中。 【示例1:
1
2
3
4
5
void f() {
union { int a; const char* p; };
a = 1;
p = "Jennifer";
}
这里`a`和`p`像普通(非成员)变量一样使用,但由于它们是联合成员,所以它们具有相同的地址。——示例结束】
Paw5zx个人理解:点击查看更多

匿名联合体的所有成员会被提升到声明匿名联合体的作用域中,即无需像u.au.p这样访问,而是直接ap访问。

2
在命名空间作用域中声明的、具有外部链接的匿名联合体,必须使用[存储类说明符]()`static`。在块作用域中声明的匿名联合体,不得使用在块变量声明中不允许的[存储类说明符]()。在类作用域中声明的匿名联合体声明,不得带有[存储类说明符]()。
Paw5zx个人理解:点击查看更多

这段话约束了匿名联合体在不同作用域下的存储类规则:

  • 如果匿名联合体出现在命名空间作用域(包括全局作用域)中,且具有外部链接(即默认情况下可被其他翻译单元访问),必须加 static:
    1
    static union { int a; float b; }; // OK,a和b具有内部链接
    这样做是为了避免匿名联合体的成员名字在全局范围造成链接冲突。
  • 如果匿名联合体是局部变量,它的行为就像普通局部变量一样:
    1
    2
    3
    4
    void f() {
    // extern union { int a; float b; }; // 非法:extern在块作用域下的匿名联合体是不允许的
    union { int x; float y; }; // OK,不需要任何存储类说明符
    }
    但是不能使用不允许用于局部变量的存储类(例如extern
  • 如果匿名联合体是类的成员,则不能有任何存储类说明符:
    1
    2
    3
    struct S {
    union { int i; float f; }; // OK
    };
3
【注1:声明了对象、指针或引用的联合体不是匿名联合体。 【示例2:
1
2
3
4
5
void f() {
union { int aa; char* p; } obj, *ptr = &obj;
aa = 1; // 错误
ptr->aa = 1; // 正确
}
对普通的`aa`进行赋值是非法的,因为成员名在联合体之外是不可见的;即便是可见的,它也没有与任何特定对象相关联。——示例结束】 ——注释结束】
Paw5zx个人理解:点击查看更多

根据定义:匿名联合体定义了一个未命名的类型,同时在当前作用域下隐式定义了一个未命名对象,这个对象的所有成员会提升到当前作用域中。

因此对于:

1
2
3
4
5
void f() {
union { int aa; char* p; } obj, *ptr = &obj;
aa = 1; // 错误
ptr->aa = 1; // 正确
}

这里虽然写了union { ... }的形式,但后面定义了obj这个对象,因此union { ... }不是匿名联合体而是一个没有名字的类型。所以aa这个成员没有提升到外层作用域,只能通过obj.aaptr->aa访问。

4
一个`union-like`类要么是一个联合体,要么是一个拥有匿名联合体作为直接成员的类。 一个`union-like`类`X`拥有一组变体成员(`variant members`)。如果`X`本身是一个联合体,那么`X`的所有非匿名联合体的非静态数据成员都是它的变体成员。此外,`X`的匿名联合体成员中的非静态数据成员也同样是`X`的变体成员。在一个联合体中,最多只能有一个变体成员带有默认成员初始化器。 【示例3:
1
2
3
4
5
6
7
8
9
10
union U {
int x = 0;
union {
int k;
};
union {
int z;
int y = 1; // error: initialization for second variant member of U
};
};
——示例结束】
Paw5zx个人理解:点击查看更多

变体成员指的是联合体内遵循互斥激活规则的成员,这些变体成员共享存储空间,同一时间只能有一个是活动的

  • 如果X是联合体,那么它的所有非匿名联合体的非静态成员都是X的变体成员
  • 如果X是拥有匿名联合体作为直接成员的类,则X的匿名联合体的中的非静态数据成员都是X的变体成员

变体成员共享同一片内存,编译器无法同时在构造函数里初始化两个会互相覆盖的成员,所以最多只能有一个变体成员带默认成员初始化器,否则就会冲突。

在构造联合体对象时,如果没有显式初始化器,则这个带默认初始化器的变体成员会被初始化,并成为活动成员。(待考证)