【C++基础】第七十一课:[拷贝控制]交换操作

自定义swap函数

Posted by x-jeff on May 12, 2023

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.交换操作

除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。对于那些与重排元素顺序的算法一起使用的类,定义swap是非常重要的。这类算法在需要交换两个元素时会调用swap。

如果一个类定义了自己的swap,那么算法将使用类自定义版本。否则,算法将使用标准库定义的swap。虽然与往常一样我们不知道swap是如何实现的,但理论上很容易理解,为了交换两个对象我们需要进行一次拷贝和两次赋值。例如,交换两个类值HasPtr对象的代码可能像下面这样:

1
2
3
HasPtr temp = v1; //创建v1的值的一个临时副本
v1 = v2; //将v2的值赋予v1
v2 = temp; //将保存的v1的值赋予v2

理论上,这些内存分配都是不必要的。我们更希望swap交换指针,而不是分配string的新副本。即,我们希望这样交换两个HasPtr:

1
2
3
string *temp = v1.ps; //为v1.ps中的指针创建一个副本
v1.ps = v2.ps; //将v2.ps中的指针赋予v1.ps
v2.ps = temp; //将保存的v1.ps中原来的指针赋予v2.ps

1.1.编写我们自己的swap函数

可以在我们的类上定义一个自己版本的swap来重载swap的默认行为。swap的典型实现如下:

1
2
3
4
5
6
7
8
9
10
class HasPtr {
	friend void swap(HasPtr&, HasPtr&);
	//其他成员定义,与之前一样
};
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
	using std::swap;
	swap(lhs.ps, rhs.ps); //交换指针,而不是string数据
	swap(lhs.i, rhs.i); //交换int成员
}

我们首先将swap定义为friend,以便能访问HasPtr的(private的)数据成员。由于swap的存在就是为了优化代码,我们将其声明为inline函数

与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。

1.2.swap函数应该调用swap,而不是std::swap

在上述代码中,using std::swap;将std命名空间中的swap函数引入了当前作用域。首先在当前作用域查找swap函数。如果HasPtr类内部没有名为swap的成员函数,并且没有对std::swap进行特化,那么继续查找外层作用域。在这种情况下,由于使用了using std::swap;声明,会找到std::swap函数并进行调用。换句话说,如果HasPtr类定义了一个成员函数swap,或者HasPtr类针对std::swap进行了特化,那么调用的就是这个特定版本的swap函数,而不是std::swap。

1.3.在赋值运算符中使用swap

定义swap的类通常用swap来定义它们的赋值运算符。这些运算符使用了一种名为拷贝并交换(copy and swap)的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:

1
2
3
4
5
6
7
8
//注意rhs是按值传递的,意味着HasPtr的拷贝构造函数
//将右侧运算对象中的string拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
	//交换左侧运算对象和局部变量rhs的内容
	swap(*this, rhs); //rhs现在指向本对象曾经使用的内存
	return *this; //rhs被销毁,从而delete了rhs中的指针
}

在这个版本的赋值运算符中,参数并不是一个引用,我们将右侧运算对象以传值方式传递给了赋值运算符。因此,rhs是右侧运算对象的一个副本。参数传递时拷贝HasPtr的操作会分配该对象的string的一个新副本。

当赋值运算符结束时,rhs被销毁,HasPtr的析构函数将执行。此析构函数delete rhs现在指向的内存,即,释放掉左侧运算对象中原来的内存。

这个技术的有趣之处是它自动处理了自赋值情况且天然就是异常安全的。它通过在改变左侧运算对象之前拷贝右侧运算对象保证了自赋值的正确。

使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。