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 離開作用域,執行析構函數被銷燬。所以,左側的運算對象中原來的內存得以釋放

​ 這個技術它自動處理了自賦值情況且天然是異常安全的

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