C++ copy and swap (拷贝交换技术)

来源:互联网 发布:linux 重启服务 编辑:程序博客网 时间:2024/05/21 12:47

写在前面

关于C++的赋值运算符的重写,effective C++上已经有足够详细的描述,但是对于拷贝交换技术只是简单的提及,作者对此的看法是不提倡。最近看了一些文章,结合stackoverflow上的解答,我认为事实上拷贝交换技术还是非常有学习和应用的必要的,其关键在于,把一切编译器可以完成的工作完全交给编译器去做,而不是由我们去手工实现,从而避免了很多错误(这些错误在存在继承关系的赋值运算符的重写过程中非常容易出现)。具体说,就是指拷贝过程和析构过程。

实现方式对比

我们先不考虑存在继承关系的类的赋值运算符重写,只考虑最简单的情况。我们知道,按照C++ primer的理解,赋值运算符应该实现两个方面的工作:1.拷贝构造函数 2. 析构函数。只有完整实现了上述两步工作,赋值运算才能够正确进行。

写法1

class A {private:    int *b;    int a;public:    A():a(0),b(nullptr){};    A(const A&rhs):a(rhs.a),b(rhs.b==nullptr?nullptr:new int(*rhs.b)){};    ~A(){        delete b;        b = nullptr;    };};

我们先给出上述类A的定义,注意到类A中数据成员的数据类型,分别是内置整型以及整型指针。据此给出了构造以及析构函数。
赋值运算符包括构造以及析构两方面,因此给出第一种定义:

A& operator=(const A& rhs) {    if(this!=&rhs) { // 防止自赋值        delete b;        this->b = new int(*rhs.b);// 可能失败        this->a = rhs.a;    }    return *this; // 返回this对象的引用}

可以看到我们的代码几乎是对拷贝构造函数和析构函数的完全复制,此外,上述代码虽然完成了自赋值的验证,但并未保障异常安全。一旦new失败,原this对象的b已经被删除,因此会引发异常。

改进写法2

effective C++ 关于本节的条款提到,无须在意自赋值,更多地考虑异常安全,异常安全得到保证,则自赋值自然得到处理。回到当前的例子,异常不安全主要在于,b对应的对象可能在异常到来之前被删除。因此我们首先保存该对象的副本,从而保证了异常安全特性,无论new是否成功,this对象中的b指针都会指向已知对象:

A& operator=(const A& rhs) {    auto orign = this->b;    this->b = new int(*rhs.b);    delete orign;    this->a = rhs.a;    return *this;}

该写法不仅是异常安全的,同时也能够处理自赋值,但冗余代码的问题仍未得到解决,在effective C++中提到,可以写一个private函数进行调用,可是,这种写法并未解决根本问题:我们在赋值运算中重复实现了拷贝构造函数和析构函数。

copy and swap (写法3)

上述方法事实上是致命的。在不考虑继承关系的复杂情况下,如果更改类A,添加数据成员,我们在修改其它构造/析构函数的同时,也必须修改赋值运算符。copy and swap技术则可以做到完全规避这一点,此外,所有调用工作由编译器自动完成,无需再做任何额外操作。
该技术的核心就是不再使用引用作为赋值运算符参数,形参将直接是对象,这样的写法将会使编译器自动调用拷贝构造函数,由于拷贝构造函数的调用,异常安全将在进入函数体之前被避免(若拷贝失败则什么都不会发生)。经过swap后的对象在离开函数体后会自动销毁,因此也就自动调用了析构函数,具体写法如下:

void swap(A& rhs) {    using std::swap;    swap(this->a,rhs.a);    swap(this->b,rhs.b);}A& operator=(A rhs) {    swap(rhs);    return *this;}

我们的代码有着显而易见的优势:所有需要考虑的问题会由编译器处理,我们无需考虑任何事项,关键是,它的正确性是显而易见而且符合逻辑的。对于类的扩展,我们除了构造函数/析构函数外,只需要修改swap函数即可。

考虑存在继承的复杂情形

本节对应的内容是effective C++ 条款12,复制对象时勿忘记复制其每一成分。 假设有如下类B继承自上述类A:

class B : public A {private:    int ab;public:    B():ab(0){}    B(const B&rhs):ab(rhs.ab){} // copy constructor    B& operator=(const B&rhs){        this->ab = rhs.ab;      // assignment operator        return *this;}};

上述写法有两个错误,首先,B的拷贝构造函数只复制了B的数据成员,对于父类A中的私有成员,并没有进行复制,因此没有做到 复制所有成员,对此拷贝构造函数需要修改为:

B(const B&rhs):A(rhs),ab(rhs.ab){} // copy constructor

同理:赋值运算符也应修改为:

    B& operator=(const B&rhs){        A::operator=(rhs);        this->ab = rhs.ab;      // assignment operator        return *this;}

对于采用拷贝交换技术的类,我们则调用其父类的swap函数:

void swap(B& rhs) {    using std::swap;    A::swap(rhs);    swap(this->ab,rhs.ab);}B& operator=(B rhs) {    swap(rhs);    return *this;}

可以看到,拷贝交换技术的简洁性和可维护性都要好于其它写法,优势明显。

原创粉丝点击