C++中安全又便捷的Swap-and-Copy-Idiom

Swap-and-copy idiom

什么是swap and copy?

我们考虑每次实现 拷贝赋值/移动赋值 函数时是很麻烦的,需要考虑很多东西,例如浅拷贝/深拷贝,例如要写big-five等。

swap-and-copy idiom 优雅地帮助赋值操作符实现两件事:

  • 避免代码冗余:不再写拷贝赋值/移动赋值 函数, 移动构造函数也可以直接用swap来写
  • 并提供强大的异常保证。

从概念上讲,它的工作原理是使用拷贝构造函数的功能创建数据的本地副本,然后使用swap函数获取拷贝的数据,将旧数据与新数据交换。然后临时副本销毁,并带走旧数据。我们得到了新数据的副本。

从一个例子看起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
// (default) constructor
dumb_array(std::size_t size = 0)
: mSize(size),
mArray(mSize ? new int[mSize]() : nullptr)
{
}

// copy-constructor
dumb_array(const dumb_array& other)
: mSize(other.mSize),
mArray(mSize ? new int[mSize] : nullptr)
{
// note that this is non-throwing, because of the data
// types being used; more attention to detail with regards
// to exceptions must be given in a more general case, however
std::copy(other.mArray, other.mArray + mSize, mArray);
}

// destructor
~dumb_array()
{
delete [] mArray;
}

private:
std::size_t mSize;
int* mArray;
};

写出他的拷贝赋值函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// the hard part
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get rid of the old data...
delete [] mArray; // (2)
mArray = nullptr; // (2) *(see footnote for rationale)

// ...and put in the new
mSize = other.mSize; // (3)
mArray = mSize ? new int[mSize] : nullptr; // (3)
std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
}

return *this;
}

出现了几个问题:

  • 自我赋值检测使得运行速度变差,代码冗余
  • 如果new int[mSize]失败,此时mArray已经删除掉了,强异常安全没有保证(强异常安全保证:如果抛出了异常,程序的状态没有发生任何改变。就像没调用这个函数一样。)

基本保证(Basic Exception Safety): 也叫无泄漏保证(No-Leak Guarantee),即发生异常时不会导致资源泄露(比如内存泄露),程序内的任何事物仍然保持在有效状态下,没有对象或数据结构会因此而破坏,所有对象都处于有效的状态,但是处于哪个状态不可预知。

强烈保证(Strong Exception Safety):如果抛出异常,程序状态不改变。就像数据库中的事务处理一样,要么成功,如果不成功,则程序回到调用之前的状态。

不抛出异常保证(No-Throw Guarantee):承诺绝不抛出异常。如果有异常发生,会在内部处理,保证不让异常逃逸。

因此我们为了保证强异常安全:先new,再删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get the new data ready before we replace the old
std::size_t newSize = other.mSize;
int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
std::copy(other.mArray, other.mArray + newSize, newArray); // (3)

// replace the old data (all are non-throwing)
delete [] mArray;
mSize = newSize;
mArray = newArray;
}

return *this;
}

但这无辜多出了新的变量来暂时存下申请空间的指针:newArray。

看起来怎么都达不到很好的效果。

我们尝试这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class dumb_array
{
public:
// ...

friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
// enable ADL (not necessary in our case, but good practice)
using std::swap;

// by swapping the members of two objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}

// ...
};

//新的写法
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)

return *this;
}

利用了std::swap的不抛出异常,保证了自己swap的不抛出异常

值传递参数的赋值函数为什么有用?

Q1: 为什么 赋值操作符 用值传参?

我们首先注意到一个重要的选择:形参实参是按值获取的,为什么?为什么不用const ref来做,然后再拷贝出一个副本(如下)?

这主要是为了优化:https://web.archive.org/web/20140113221447/http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

1
2
3
4
5
6
7
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);

return *this;
}

Q2:怎么起作用的

temp是个临时变量,把我们this的旧数据扔给temp后无需记住需要delete等操作,他离开作用域会通过析构函数完成。

Q3:Why public friend swap?

https://stackoverflow.com/questions/5695548/public-friend-swap-member-function

移动构造函数也可以用swap!

C++11后我们有了移动语义,需要考虑移动构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
class dumb_array
{
public:
// ...

// move constructor
dumb_array(dumb_array&& other) noexcept
: dumb_array()
{
swap(*this, other);
}

};

我们直接使用swap即可,进行资源的转换,这正是swap本身的含义。

移动赋值函数用swap吗?

我们不用专门写一个移动赋值函数,因为 考虑当一个右值对象 会调用到我们写的赋值操作符

1
2
3
4
5
6
7
//新的写法
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)

return *this;
}

参数捕获右值会调用移动构造,然后this通过swap拿到新数据,把旧数据给到other这个临时变量,超过作用域后,other调用析构函数,把右值对象的资源也释放掉。

好在哪儿?

  • 解决代码冗余问题:
    • 我们往往需要写big-five: copy-constructor, move-constructor, copy-assignment,move-assignment,destructor。 现在我们仅仅需要实现 copy-constructor,move-constructor,destructor即可,其他利用我们实现的swap和 赋值操作符 即可完成
    • 同时move-constructor实现也变得非常简单,直接利用swap语义
  • 强异常安全保证