现代C++新特性 非受限联合类型

现代C++新特性 非受限联合类型,第1张

   文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载 

1.联合类型在C++中的局限

在编程的问题中,用尽量少的内存做尽可能多的事情一直都是一个重要的课题。

C++中的联合类型(union)可以说是节约内存的一个典型代表。

因为在联合类型中多个对象可以共享一片内存,相应的这片内存也只能由一个对象使用,例如:

#include  

union U {
    int x1;  
    float x2;
};

int main(int argc, char** argv)
{
    U u;
    u.x1 = 5;
    cout << u.x1 << endl;  
    cout << u.x2 << endl;

    u.x2 = 5.0;
    cout << u.x1 << endl; 
    cout << u.x2 << endl;
    return 0;
}

在上面的代码中联合类型U里的成员变量x1和x2共享同一片内存,所以修改x1的值,x2的值也会发生相应的变化,反之亦然。

不过需要注意的是,虽然x1和x2共享同一片内存,但是由于CPU对不同类型内存的理解存在区别,因此即使内存相同也不能随意使用联合类型的成员变量,而是应该使用之前初始化过的变量。

像这样多个对象共用一片内存的情况在内存紧缺时是非常实用的。

不过令人遗憾的是,过去的联合类型在C++中的使用并不广泛,因为C++中的大多数对象不能成为联合类型的成员。

过去的C++标准规定,联合类型的成员变量的类型不能是一个非平凡类型,也就是说它的成员类型不能有自定义构造函数,比如:

union U {
    int x1;   
    float x2;  
    string x3;
};

上面的代码是无法通过编译的,因为x3存在自定义的构造函数,所以它是一个非平凡类型。

但事实上,面向对象的编程中一个好的类应该隐藏内部的细节,这就要求构造函数足够强大并正确地初始化对象的内部数据结构,而编译器提供的构造函数往往不具备这样的能力,于是大多数情况下,我们会为自己的类添加一个好用的构造函数,但是这种良好的设计却造成了这个类型无法在联合类型中使用。

基于这些问题,C++委员会在新的提案当中多次强调“我们没有任何理由限制联合类型使用的类型”。

在这份提案中有一段话非常好地阐述了C++的设计理念,同时也批判了联合类型的限制对这种理念的背叛,这段话是这样说的:当面对一个可能被滥用的功能时,语言的设计者往往有两条路可走,一是为了语言的安全性禁止此功能,另外则是为了语言的能力和灵活性允许这个功能,C++的设计者一般会采用后者。

但是联合类型的设计却与这一理念背道而驰。

这种限制完全没有必要,去除它可以让联合类型更加实用。

回味这段话,C++的设计确实一直遵从这样的理念,我们熟悉的指针就是一个典型的代表!

2. 使用非受限联合类

为了让联合类型更加实用,在C++11标准中解除了大部分限制,联合类型的成员可以是除了引用类型外的所有类型。

不过这样的修改引入了另外一个问题,如何精确初始化联合类型成员对象。

这一点在过去的联合类型中不是一个问题,因为对于平凡类型,编译器只需要对成员对象都执行编译器提供的默认构造即可,虽然从同一内存多次初始化的角度来说这是不正确的,但是从结果上看没有任何问题。

现在情况发生了变化,由于允许非平凡类型的存在,对所有成员一一进行默认构造明显是不可取的,因此我们需要有选择地初始化成员对象。

实际上,让编译器去选择初始化本身也是不合适的,这个事情应该交给程序员来做。

基于这些考虑,在C++11中如果有联合类型中存在非平凡类型,那么这个联合类型的特殊成员函数将被隐式删除,也就是说我们必须自己至少提供联合类型的构造和析构函数,比如:

#include  
#include  
#include  

union U {
    U() {}        // 存在非平凡类型成员,必须提供构造函数 
    ~U() {}       // 存在非平凡类型成员,必须提供析构函数   
    int x1;
    float x2;
    string x3;
    vector x4;
};

int main(int argc, char** argv)
{
    U u;
    u.x3 = "hello world";
    cout << u.x3 << endl;
    return 0;
}

在上面的代码中,由于x3和x4的类型string和vector是非平凡类型,因此U必须提供构造和析构函数。

虽然这里提供的构造和析构函数什么也没有做,但是代码依然可以成功编译。

不过请注意,能够编译通过并不代表没有问题,实际上这段代码会运行出错,因为非平凡类型x3并没有被构造,所以在赋值 *** 作的时候必然会出错。

现在修改一下代码:

#include  
#include  
#include  

union U {
    U() : x3() {}
    ~U()
    {
        x3.~basic_string();
    }
    int x1;
    float x2;
    string x3;
    vector x4;
};

int main(int argc, char** argv)
{
    U u;
    u.x3 = "hello world";
    cout << u.x3 << endl;
    return 0;
}

在上面的代码中,我们对联合类型U的构造和析构函数进行了修改。

其中在构造函数中添加了初始化列表来构造x3,在析构函数中手动调用了x3的析构函数。

前者很容易理解,而后者需要注意,联合类型在析构的时候编译器并不知道当前激活的是哪个成员,所以无法自动调用成员的析构函数,必须由程序员编写代码完成这部分工作。

现在联合类型U的成员对象x3可以正常工作了,但是这种解决方案依然存在问题,因为在编写联合类型构造函数的时候无法确保哪个成员真正被使用。

具体来说,如果在main函数内使用U的成员x4,由于x4并没有经过初始化,因此会导致程序出错:

#include  
#include  
#include  

union U {
    U() : x3() {}
    ~U()
    {
        x3.~basic_string();
    }

    int x1;
    float x2;
    string x3;
    vector x4;
};

int main(int argc, char** argv)
{
    U u;
    u.x4.push_back(58);
    return 0;
}

基于这些考虑,我还是比较推荐让联合类型的构造和析构函数为空,也就是什么也不做,并且将其成员的构造和析构函数放在需要使用联合类型的地方。

让我们继续修改上面的代码:

#include  
#include  
#include  

union U {
    U() {}
    ~U() {}
    int x1;
    float x2;
    string x3;
    vector x4;
};
int main(int argc, char** argv)
{
    U u;
    new(&u.x3) string("hello world");
    cout << u.x3 << endl;

    u.x3.~basic_string();
    new(&u.x4) vector;
    u.x4.push_back(58);
    cout << u.x4[0] << endl;
    u.x4.~vector();
    return 0;
}

请注意,上面的代码用了placement new的技巧来初始化构造x3和x4对象,在使用完对象后手动调用对象的析构函数。

通过这样的方法保证了联合类型使用的灵活性和正确性。

后简单介绍一下非受限联合类型对静态成员变量的支持。

联合类型的静态成员不属于联合类型的任何对象,所以并不是对象构造时被定义的,不能在联合类型内部初始化。

实际上这一点和类的静态成员变量是一样的,当然了,它的初始化方法也和类的静态成员变量相同:

#include  

union U {
    static int x1;
};

int U::x1 = 42;

int main(int argc, char** argv)
{
    cout << U::x1 << endl;
    return 0;
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/674012.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存