联合体声明

来自cppreference.com
< cpp‎ | language

联合体是特殊的类类型,它在一个时刻只能保有其一个非静态数据成员

联合体声明的类说明符与类或结构体的声明相似:

union 属性 类头名 { 成员说明 }
属性 - (C++11 起) 任意数量属性的可选序列
类头名 - 被定义的联合体的名字。可以前附 嵌套名说明符(名字与作用域解析运算符的序列,以作用域解析运算符结尾)。可以忽略名字,此时联合体是无名
成员说明 - 访问说明符、成员对象和成员函数的声明与定义的列表。

联合体可以拥有成员函数(包含构造函数和析构函数),但不能有虚函数。

联合体不能有基类且不能用作基类。

联合体不能拥有引用类型的非静态数据成员。

联合体不能含有带非平凡特殊成员函数(复制构造函数复制赋值运算符或析构函数)的非静态数据成员。

(C++11 前)

如果联合体含有带非平凡特殊成员函数(复制/移动构造函数,复制/移动赋值,或析构函数)的非静态数据成员,那么该联合体中的那些函数默认被弃置,且需要程序员显式定义。

如果联合体含有带非平凡默认构造函数的非静态数据成员,那么该联合体的默认构造函数默认被弃置,除非该联合体的变体成员拥有一个默认成员初始化器。

最多只有一个变体成员可以拥有默认成员初始化器

(C++11 起)

正如结构体的声明中一般,联合体的默认成员访问是 public

解释

联合体的大小仅足以保有其最大的数据成员。其他数据成员在该最大成员的一部分相同的字节分配。分配的细节是实现定义的,且读取并非最近写入的联合体成员是未定义行为。许多编译器以非标准语言扩展实现读取联合体的不活跃成员的能力。

#include <iostream>
#include <cstdint>
union S
{
    std::int32_t n;     // 占用 4 字节
    std::uint16_t s[2]; // 占用 4 字节
    std::uint8_t c;     // 占用 1 字节
};                      // 整个联合体占用 4 字节
 
int main()
{
    S s = {0x12345678}; // 初始化首个成员,s.n 现在是活跃成员
    // 于此点,从 s.s 或 s.c 读取是未定义行为
    std::cout << std::hex << "s.n = " << s.n << '\n';
    s.s[0] = 0x0011; // s.s 现在是活跃成员
    // 在此点,从 n 或 c 读取是 UB 但大多数编译器都对其有定义
    std::cout << "s.c 现在是 " << +s.c << '\n' // 11 或 00,取决于平台
              << "s.n 现在是 " << s.n << '\n'; // 12340011 或 00115678
}

可能的输出:

s.n = 12345678
s.c 现在是 0
s.n 现在是 115678

每个成员的分配都如同它是类的唯一成员一样。

如果联合体的成员是拥有用户定义的构造函数和析构函数的类,那么切换其活跃成员通常需要显式析构函数和布置 new:

#include <iostream>
#include <string>
#include <vector>
 
union S
{
    std::string str;
    std::vector<int> vec;
    ~S() {} // 需要知道哪个成员活跃,只能在联合体式的类中做到
};          // 整个联合体占有 max(sizeof(string), sizeof(vector<int>)) 的内存
 
int main()
{
    S s = {"Hello, world"};
    // 在此点,从 s.vec 读取是未定义行为
    std::cout << "s.str = " << s.str << '\n';
    s.str.~basic_string();
    new (&s.vec) std::vector<int>;
    // 现在,s.vec 是联合体的活跃成员
    s.vec.push_back(10);
    std::cout << s.vec.size() << '\n';
    s.vec.~vector();
}

输出:

s.str = Hello, world
1
(C++11 起)

如果两个联合体成员都是标准布局类型,那么在任何编译器上检验其公共子序列都有良好定义。

成员生存期

联合体成员的生存期从该成员被设为活跃(active)时开始。如果之前已经有另一成员活跃,那么它的生存期终止。

当联合体的活跃成员通过形式为 E1 = E2 的复制表达式(使用内建赋值运算符或平凡的赋值运算符)切换时,对于 E1 中的各个成员访问和数组下标子表达式中出现的,其类型并非拥有非平凡或弃置的默认构造函数的类的每个联合体成员 X,如果 X 的修改在类型别名使用规则下会具有未定义行为,那么在所指名的存储中隐式创建一个 X 类型的对象;不进行初始化,且其生存期的开始按顺序晚于其左右的操作数的值计算,而早于赋值。

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:"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 在其生存期外被修改,
             // "y.x.b" 指名 y.x,但 X 的默认构造函数被弃置,
             // 所以联合体成员 y.x 的生存期不会隐式开始
}

联合体类型的平凡移动构造函数、移动赋值运算符、 (C++11 起)复制构造函数和复制赋值运算符复制对象表示。如果源与目标不是同一对象,那么这些特殊成员函数在复制前开始每个内嵌于目标的并对应内嵌于源的对象(除了既非目标的子对象亦不拥有隐式生存期类型的对象)的生存期。否则,它们不做任何事。在经由平凡特殊成员函数构造或赋值后,两个联合体对象拥有相同的对应活跃成员(如果存在)。

匿名联合体

匿名联合体是不同时定义任何变量(包括联合体类型的对象、引用或指向联合体的指针)的无名的联合体定义。

union { 成员说明 } ;

匿名联合体有更多限制:它们不能有成员函数,不能有静态数据成员,且所有数据成员必须公开。只能声明非静态数据成员,外加static_assert 声明 (C++11 起)

匿名联合体的成员被注入到它的外围作用域中(而且不得与其中声明的其他名字冲突)。

int main()
{
    union
    {
        int a;
        const char* p;
    };
    a = 1;
    p = "Jennifer";
}

命名空间作用域的匿名联合体必须声明为 static,除非它们在无名命名空间出现。

联合体式的类

联合体式的类(union-like class)是联合体,或是至少拥有一个匿名联合体成员的(非联合)类。联合体式的类拥有一组变体成员(variant member)

  • 其成员匿名联合体的非静态数据成员;
  • 另外,如果联合体式的类是联合体,则为其并非匿名联合体的非静态数据成员。

联合体式的类可用于实现带标签联合体(tagged union)

#include <iostream>
 
// S 拥有一个非静态数据成员(tag),三个枚举项成员(CHAR、INT、DOUBLE),
// 和三个变体成员(c、i、d)
struct S
{
    enum{CHAR, INT, DOUBLE} tag;
    union
    {
        char c;
        int i;
        double d;
    };
};
 
void print_s(const S& s)
{
    switch(s.tag)
    {
        case S::CHAR: std::cout << s.c << '\n'; break;
        case S::INT: std::cout << s.i << '\n'; break;
        case S::DOUBLE: std::cout << s.d << '\n'; break;
    }
}
 
int main()
{
    S s = {S::CHAR, 'a'};
    print_s(s);
    s.tag = S::INT;
    s.i = 123;
    print_s(s);
}

输出:

a
123

C++ 标准库包含 std::variant,它可以取代联合体和联合体式的类的大多数用途。上例可重写为

#include <variant>
#include <iostream>
 
int main()
{
    std::variant<char, int, double> s = 'a';
    std::visit([](auto x){ std::cout << x << '\n';}, s);
    s = 123;
    std::visit([](auto x){ std::cout << x << '\n';}, s);
}

输出:

a
123
(C++17 起)

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 1940 C++11 匿名联合体只允许非静态数据成员 也允许 static_assert

参阅