(广义的)联合体

在C++98(或更早)版本中,union的成员类型,必须不含自定义构造/析构函数或者赋值操作符。

union U {
  int m1;
  complex m2;    //错误(明显的):complex拥有构造函数
  //错误(不那么明显):string的内部数据只能严格地由其构造函数,
  // 拷贝构造函数,和析构函数去维护
  string m3;
};

亦即:

U u;        // 使用哪个成员的构造函数呢?
u.m1 = 1;     // 给整型成员赋值
string s = u.m3;    //灾难:从string成员拷贝

显而易见,把值写入一个成员,之后又读取另外一个成员的做法是给自己找麻烦,然而人们往往并非刻意而为(多因失误导致) 。

C++11对union的限制条件进行了放宽,以允许更灵活广泛的成员类型。其中值得特别指出的是,带有自定义构造函数/析构函数的类型现在也可作为union的成员了。此外,为使“灵活”不至成为脱缰野马,新标准又特别引入了“第四条军规”(译注:参见下文),并倡导使用“可识别union”(译注:参见下文Widget样例以及Boost::Any和Boost::Variant)。

C++11中的对union的限制条件重新定义如下:

  • 不含虚函数(与C++98相同)
  • 不含引用成员(与C++98相同)
  • 没有基类(与C++98相同)
  • 若union的某个成员的类型含有自定义构造/拷贝/析构函数,那么该union的相应构造/拷贝/析构函数将会被自动“禁用”(译注:在C++11中我们可以使用delete关键字来“禁用”构造/析构函数),随之而来的后果是:该union不能被实例化成对象。(新增的所谓“第四条军规”)

例如:

union U1 {
  int m1;
  complex m2;    // ok
};

union U2 {
  int m1;
  string m3;    // ok
};

上述代码看起来容易出问题(译注:例如有人实例化了U2类型的对象并给其m1成员赋值之后,当union析构时,m3的析构函数可能会crash),但是有了第四条军规,刚才的隐含问题便迎刃而解(译注:通过编译错误)。即:

U1 u;          // ok
u.m2 = {1,2};    // ok:给complex成员赋值
U2 u2;   // 编译错误: string类含有析构函数,因而U2的析构函数已被自动禁用
    //(译注:析构函数被禁用意味着不允许在栈上实例化U2对象,否则无法析构)
U2 u3 = u2;    // 编译错误:string类含有拷贝构造函数,
    // 因而U2类型同样也不能被拷贝构造

这样看来,先前定义的U2几乎没什么实际用途了。唯一可能用到这种奇葩union的地方是,把它嵌到结构体里并且额外记录其“当值”成员类型——也就是所谓的“可识别union”。样例如下:

class Widget {  // 用union存储的“三态”Widget
  private:
    // 用以实现“可识别union”的“当值”类型标记
    enum class Tag { point, number, text } type;
    union {    // 三态的具体存储形式
      point p;    // point类含有构造函数
      int i;
      string s;   // string类含有默认的构造/拷贝/析构函数
    };
    // ...
    // 由于string中存在拷贝函数,所以需要“手工拷贝”union
    Widget& operator=(const Widget& w)
    {
      // 译注:从text态到text态
      if (type==Tag::text && w.type==Tag::text) {
        s = w.s;    // 直接使用string的赋值运算符
        return *this;
      }
      // 译注:从text态到其他态,意味着union不再用于存放string
      //    由于union的拷贝函数已被自动禁用,
      //    所以需要有人手工释放string原先所占资源
      if (type==Tag::text) s.~string();  // 此处需要显式析构

      switch (w.type) {
      case Tag::point: p = w.p; break;  // 普通的拷贝
      case Tag::number: i = w.i; break;
      // 译注:C++98/03标准中的的原地拷贝构造语法
      case Tag::text: new(&s)(w.s); break;  // placement new
      }
      type = w.type;
      return *this;
    }
  };
};

其他参考文献:

(翻译:张潇)