前提
有 HasPtr 类:
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) {}
HasPtr(const HasPtr &sour):
ps(new std::string(*sour.ps)), i(sour.i) { }
HasPtr &operator=(const HasPtr &sour);
~HasPtr() { delete(ps); }
private:
std::string *ps;
int i;
};
swap 操作
除了定义拷贝控制成员,管理资源的类通常还定义一个名为 swap 的函数。对于那些重排元素顺序的算法一起使用类,定义 swap 是非常重要的。这类算法在交换两个元素时会调用 swap。
如果一个类定义了自己的 swap,那么算法将使用类自定义版本。否则,算法将使用标准库定义的 swap。
我们很容易想到以下的数据交换方式:
HasPtr temp = v1;
v1 = v2;
v2 = temp;
但是我们会发现,以上操作会进行多次拷贝。当我们的 HasPtr 的数据量很大时,这样的拷贝是非常耗时的。除此之外,拷贝并且还会分配新的内存。
理论上,这些内存分配都是不必要的。我们更希望 swap 交换指针,而不是分配 string 的新副本。我们更希望这样交换两个 HasPtr:
string *temp = v1.ps;
v1.ps = v2.ps;
v2.ps = temp;
编写我们自己的 swap 函数
可以在我们的类上定义一个自己版本的 swap 来重载 swap 的默认行为。典型实现如下:
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);
}
与拷贝控制成员不同,swap 并不是必要的。但是,对于分配了资源的类,定义 swap 可能是一种优化手段。
swap 函数应该调用 swap,而不是 std::swap
上面的 swap 有一个很微妙的地方:虽然这一点并没有体现出来,但一般情况下它非常重要——swap 函数中调用的 swap 不是 std::swap。
如果一个类的成员有自己类型特定的 swap函数,调用 std::swap 就是错误的。假如,我们有一个 Foo 类,它的成员是 HasPtr h。如果我们未定义 Foo 版本的 swap,那么就会使用标准库版本的 swap。如我们知道的,标准库 swap 对 HasPtr 管理进行了不必要的拷贝。
我们可以为 Foo 定义自己的 swap 来避免这些拷贝,但是如果这样编写:
void swap(Foo &lhs, Foo &rhs) {
std::swap(lhs.h, rhs.h); // 这个函数使用了标准库版本的 swap,而不是 HasPtr 版本
}
如上说的,这个函数使用了标准库版本的 swap,而不是 HasPtr 版本。如果我们希望调用 HasPtr 对象定义的版本,应该这样:
void swap(Foo &lhs, Foo &rhs) {
using std::swap;
swap(lhs.h, rhs.h);
}
我们调用时对 swap 应该都是未加限定的。即每个调用都应该是 swap,而不是 std::swap。如果存在特定类型的 swap 版本,其匹配程度会由于 std 中的版本。(至于为什么函数中 using std::swap 并没有隐藏 HasPtr 版本的 swap 声明,这里不加以解释)
在赋值运算符中使用 swap
定义 swap 的类通常用 swap 来定义它们的赋值运算符。这些运算符使用了一种名为拷贝并交换的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:
// 注意 rhs 是右侧运算对象的一个副本
HasPtr& operator= (HasPtr rhs) {
// 交换左侧对象和 rhs 的内容
swap(*this, rhs); // 交换后 rhs 指向左侧对象原来的内存
return *this;
}
在这个版本的赋值运算符中,参数不是引用,这很重要。rhs 会从右侧运算对象拷贝初始化而来。
我们可以发现,swap 交换了左侧对象和 rhs,即左侧运算对象中原来保存的指针与 rhs 中的交换了。当我们的函数结束时,rhs 离开作用域,执行析构函数被销毁。所以,左侧的运算对象中原来的内存得以释放。
这个技术它自动处理了自赋值情况且天然是异常安全的。