C++ 慣用法之 Copy-Swap 拷貝交換
這是“C++ 慣用法”合集的第 3 篇,前面 2 篇分別介紹了 RAII 和 PIMPL 兩種慣用法:
- RAII: Resouce Acquistion Is Initialization
- PIMPL:Pointer To Implemetation
正式介紹 Copy-Swap 之前,先看下《劍指 Offer》里的第??題:
如下為型別 CMyString 的宣告,請為該型別添加賦值運算子函式,
class CMyString {
public:
CMyString(char* pData = https://www.cnblogs.com/tengzijian/archive/2023/07/08/nullptr);
CMyString(const CMyString& str);
~CMyString();
private:
char* m_pData;
};
這道題目雖然基礎,但考察點頗多,有區分度:
- 回傳值型別應為參考型別,否則將無法支持形如
s3 = s2 = s1的連續賦值 - 形參型別應為 const 參考型別
- 無資源泄露,正確釋放賦值運算子左側的物件的資源
- 自賦值安全,能夠正確處理
s1 = s1的陳述句 - 考慮例外安全
解法 1
CMyString& operator=(const CMyString& str)
{
if(this == &str)
return *this;
delete[] m_pData;
m_pData = https://www.cnblogs.com/tengzijian/archive/2023/07/08/nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
上面代碼有些細節需要注意:
- 洗掉陣列使用
delete[]運算子 strlen計算長度不含字串末尾的結束符\0strcpy會拷貝結束符\0
解法 1 滿足考察點中除例外安全外的所有要求:new 的時候可能由于記憶體不足拋例外,但此時賦值運算子左側的的物件已被釋放,m_pData 為空指標,導致左側物件處于無效狀態,
解決方案:只要先 new 分配空間,再 delete 釋放原來的空間即可,這樣可以保證即使 new 失敗拋例外,賦值運算子左側物件也尚未修改,仍處于有效狀態,
解法 2
《劍指 Offer》中給出了更好的解法:先創建賦值運算子右側物件的一個臨時副本,然后交換賦值運算子左側物件和該臨時副本的 m_pData,當臨時物件 strTemp 離開作用域時,自動呼叫其解構式,釋放 m_pData 指向的資源(即賦值運算子左側物件原來的記憶體):
CMyString& operator=(const CMyStirng& str)
{
if(this != &str)
{
CMyString strTemp(str);
char* pTemp = m_pData;
m_pData = https://www.cnblogs.com/tengzijian/archive/2023/07/08/strTemp.m_pData;
strTemp.m_pData = pTemp;
}
return *this;
}
解法 2 巧妙地利用了類原本的拷貝構造、解構式自動進行資源管理,同時又不涉及底層的 new[]/delete[] 操作,可讀性更強,也不容易出錯,
解法 2 是 Copy-Swap 的雛形,C++ 中管理資源類通常會定義自己的 swap 函式,與其他拷貝控制成員(拷貝/移動構造、拷貝/移動賦值運算子、析構)不同,swap 不是必須,但卻是重要的優化手段,以下是使用 Copy-Swap 慣用法的解法:
解法 3
class CMyString {
friend void Swap(CMyString& lhs, CMyString& rhs) noexcept
{
// 對 CMyString 的成員逐一交換
std::swap(lhs.m_pData, rhs.m_pData);
}
// ...
};
CMyString(CMyString&& str) : CMyString()
{
Swap(*this, str);
}
CMyString& operator=(CMyStirng str)
{
Swap(*this, str);
return *this;
}
這里有幾點需要注意:
- 拷貝賦值運算子的形參型別不再是 const 參考,因為 Copy-Swap 需要先對賦值運算子右側物件進行拷貝,這里直接使用值傳遞,這樣一來,也使得 Copy-Swap 天然地例外安全、自賦值安全,
- 例外安全:進入函式
operator=()之前,先進行拷貝 - 自賦值安全:形參是一個新創建的臨時物件,永遠不可能是物件自身
- 例外安全:進入函式
- 不需要額外實作移動賦值運算子:如果賦值運算子右側是一個右值,則自動呼叫 CMyString 的移動構造來構造形參
這還沒完...
標準庫 std::swap 及 ADL
C++ 標準庫也提供了 swap 函式,理論上需要一次拷貝,兩次賦值:
void swap(CMyString& lhs, CMyString& rhs)
{
CMyString tmp(lhs);
lhs = rhs;
rhs = tmp;
}
其中 CMyString tmp(lhs) 會呼叫 CMyString 的拷貝構造進行深拷貝,效率上不如 CMyString 類自己實作的直接交換指標的效率高,
在進行 swap(v1, v2) 的呼叫時,如果類實作了自己的 swap 版本,其匹配程度優于標準庫的版本,如果類沒有定義自己的 swap,則使用標準庫的 swap,這種查找匹配方式被稱為 ADL(Argument-Dependent Lookup),
注意不能使用 std::swap 形式,因為這樣會強制使用標準庫的 swap,正確的做法是提前使用 using std::swap 宣告,而后續所有的 swap 都應該是不加限制的(這一點剛好和 std::move 相反):
void swap(Bar& lhs, Bar& rhs)
{
using std::swap;
swap(lhs.m1, rhs.m1);
swap(lhs.m2, rhs.m2);
swap(lhs.m3, rhs.m3);
}
最終的結果
class CMyString {
friend void swap(CMyString& lhs, CMyString& rhs) noexcept
{
// 對 CMyString 的成員逐一交換
using std::swap;
swap(lhs.m_pData, rhs.m_pData);
}
// ...
};
CMyString(CMyString&& str) : CMyString()
{
swap(*this, str);
}
CMyString& operator=(CMyStirng str)
{
swap(*this, str);
return *this;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/556843.html
標籤:其他
下一篇:返回列表
