C++拷贝控制:swap操作 (应用于重载赋值运算符,程序优化等)

前提

​ 有 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 离开作用域,执行析构函数被销毁。所以,左侧的运算对象中原来的内存得以释放

​ 这个技术它自动处理了自赋值情况且天然是异常安全的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章