C++11 非受限联合体

C++11 非受限联合体,在C/C++中,联合体(Union)是一种构造类型的数据结构。在一个联合体内,我们可以定义多种不同的数据类型,这些数据将会共享相同内存空间,这在一些需要复用内存的情况下,可以达到节省空间的目的。不过,根据C++98标准,并不是所有的数据类型都能够成为联合体的数据成员。我们先来看一段代码:
代码清单3-38

struct Student{
    Student(bool g, int a): gender(g), age(a){}
    bool gender;
    int age;
};
union T {
    Student s;  //  编译失败,不是一个POD类型
    int id;
    char name[10];
};

我们声明了一个Student的类型。根据在POD类型中学习到的知识,由于Student自定义了一个构造函数,所以该类型是非POD的。在C++98标准中,union T是无法通过编译的。事实上,除了非POD类型之外,C++98标准也不允许联合体拥有静态或引用类型的成员。这样虽然可能在一定程度上保证了和C的兼容性,不过也为联合体的使用带来了很大的限制。

而且通过长期的实践应用证明,C++98标准对于联合体的限制是完全没有必要的。随着C++的发展,C与C++在一些方面存在着事实的不同。比如典型的“复数”类型complex,由于C语言中的复数遵从代码运行平台的二进制接口的规定,通常是一个平台上的内置类型。而在C++中,复数则常常是一个模板来实现的。这样一来,形如下面这样的C++声明,通常C++98编译器认为是不合法的。

union {
    double d;
    complex<double> cd;    //错误,complex不是一个POD类型
};

形如下面这样的C声明则被认为是合法的代码。

union {
    double d;
    complex cd;
  };

这样一来,联合体保持与C一定程度上的兼容的特性也渐渐形同虚设。因此,在新的C++11标准中,取消了联合体对于数据成员类型的限制。标准规定,任何非引用类型都可以成为联合体的数据成员,这样的联合体即所谓的非受限联合体(Unrestricted Union)。所以如果使用g++或者clang++中的-std=c++11选项编译代码清单3-38的例子,是能够通过的。此外,联合体拥有静态成员(在非匿名联合体中)的限制,也在C++11新标准中被删除了。不过从实践中,我们发现C++11的规则不允许静态成员变量的存在(否则所有该类型的联合体将共享一个值)。而静态成员函数存在的唯一作用,大概就是为了返回一个常数,我们可以看看代码清单3-39所示的例子。
代码清单3-39

#include <iostream>
using namespace std;
union T{ static long Get() { return 32; } };
int main(){
    cout << T::Get() << endl;
}

我们就定义了一个有静态成员函数的联合体。不过看起来这里的union T更像是一个作用域限制符,并没有太大的实用意义。

在C++98中,标准规定了联合体会自动对未在初始化成员列表中出现的成员赋默认初值。然而对于联合体而言,这种初始化常常会带来疑问,因为在任何时刻只有一个成员可以是有效的,如代码清单3-40所示。
代码清单3-40

union T{
    int x;
    double d;
    char b[sizeof(double)];
};
T t = {0};    // 到底是初始化第一个成员还是所有成员呢?

代码清单3-40中使用了花括号组成的初始化列表,试图将成员变量x初始化为零,即整个联合体的数据t中低位的4字节被初始化为0,然而实际上,t所占的8个字节将全部被置0。

而在C++11中,为了减少这样的疑问,标准会默认删除一些非受限联合体的默认函数。比如,非受限联合体有一个非POD的成员,而该非POD成员类型拥有有非平凡的构造函数,那么非受限联合体成员的默认构造函数将被编译器删除。其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将遵从此规则。我们可以看看代码清单3-41所示的这个例子。
代码清单3-41

#include <string>
using namespace std;
union T {
    string s;    // string有非平凡的构造函数
    int n;
};
int main() {
    T t;          // 构造失败,因为T的构造函数被删除了
}

在代码清单3-41中,联合体T拥有一个非POD的成员s。而string有非平凡的构造函数,因此T的构造函数被删除,其类型的变量t也就无法声明成功。解决这个问题的办法是,由程序员自己为非受限联合体定义构造函数。通常情况下,placement new会发挥很好的作用,如代码清单3-42所示。
代码清单3-42

#include <string>
using namespace std;
union T {
    string s;
    int n;
public:
    // 自定义构造函数和析构函数
    T(){ new (&s) string; }
    ~T() { s.~string(); }
};
int main() {
    T t;          // 构造析构成功
}

我们定义了union T的构造和析构函数。构造时,采用placement new将s构造在其地址&s上。这里placement new的唯一作用只是调用了一下string的构造函数。而在析构时,又调用了string的析构函数。读者必须注意的是,析构的时候union T也必须是一个string对象,否则可能导致析构的错误(或者让析构函数为空,至少不会造成运行时错误)。这样一来,变量t的声明就可以通过编译了。

匿名非受限联合体可以运用于类的声明中,这样的类也被称为“枚举式的类”(union-like class)。
代码清单3-43

#include <cstring>
using namespace std;
struct Student{
    Student(bool g, int a): gender(g), age(a){}
    bool gender;
    int age;
};
class Singer {
public:
    enum Type {STUDENT, NATIVE, FOREIGNER};
    Singer(bool g, int a): s(g, a) { t = STUDENT; }
    Singer(int i): id(i) { t = NATIVE; }
    Singer(const char* n, int s) {
        int size = (s > 9) ? 9 : s;
        memcpy(name, n, size);
        name[s] = '\0';
        t = FOREIGNER;
    }
    ~Singer() {}
private:
    Type t;
    union {      // 匿名的非受限联合体
        Student s;
        int id;
        char name[10];
    };
};
int main(){
Singer(true, 13);
Singer(310217);
Singer("J Michael", 9);
}

我们也把匿名非受限联合体成为类Singer的“变长成员”(variant member)。可以看到,这样的变长成员给类的编写带来了更大的灵活性,这是C++98标准中无法达到的。

酷客教程相关文章:

赞(0)

评论 抢沙发

评论前必须登录!