C++ 惯用法之 Copy-Swap 拷贝交换

打印 上一主题 下一主题

主题 940|帖子 940|积分 2820

C++ 惯用法之 Copy-Swap 拷贝交换

这是“C++ 惯用法”合集的第 3 篇,前面 2 篇分别介绍了 RAII 和 PIMPL 两种惯用法:
正式介绍 Copy-Swap 之前,先看下《剑指 Offer》里的第☝️题:
如下为类型 CMyString 的声明,请为该类型添加赋值运算符函数。
  1. class CMyString {
  2. public:
  3.   CMyString(char* pData = nullptr);
  4.   CMyString(const CMyString& str);
  5.   ~CMyString();
  6. private:
  7.   char* m_pData;
  8. };
复制代码
这道题目虽然基础,但考察点颇多,有区分度:

  • 返回值类型应为引用类型,否则将无法支持形如 s3 = s2 = s1 的连续赋值
  • 形参类型应为 const 引用类型
  • 无资源泄露,正确释放赋值运算符左侧的对象的资源
  • 自赋值安全,能够正确处理 s1 = s1 的语句
  • 考虑异常安全
解法 1
  1. CMyString& operator=(const CMyString& str)
  2. {
  3.     if(this == &str)
  4.         return *this;
  5.     delete[] m_pData;
  6.     m_pData = nullptr;
  7.     m_pData = new char[strlen(str.m_pData) + 1];
  8.     strcpy(m_pData, str.m_pData);
  9.     return *this;
  10. }
复制代码
上面代码有些细节需要注意:

  • 删除数组使用 delete[] 运算符
  • strlen 计算长度不含字符串末尾的结束符 \0
  • strcpy 会拷贝结束符 \0
解法 1 满足考察点中除异常安全外的所有要求:new 的时候可能由于内存不足抛异常,但此时赋值运算符左侧的的对象已被释放,m_pData 为空指针,导致左侧对象处于无效状态。
解决方案:只要先 new 分配空间,再 delete 释放原来的空间即可。这样可以保证即使 new 失败抛异常,赋值运算符左侧对象也尚未修改,仍处于有效状态。
解法 2

《剑指 Offer》中给出了更好的解法:先创建赋值运算符右侧对象的一个临时副本,然后交换赋值运算符左侧对象和该临时副本的 m_pData,当临时对象 strTemp 离开作用域时,自动调用其析构函数,释放 m_pData 指向的资源(即赋值运算符左侧对象原来的内存):
  1. CMyString& operator=(const CMyStirng& str)
  2. {
  3.     if(this != &str)
  4.     {
  5.         CMyString strTemp(str);
  6.         char* pTemp = m_pData;
  7.         m_pData = strTemp.m_pData;
  8.         strTemp.m_pData = pTemp;
  9.     }
  10.     return *this;
  11. }
复制代码
解法 2 巧妙地利用了类原本的拷贝构造、析构函数自动进行资源管理,同时又不涉及底层的 new[]/delete[] 操作,可读性更强,也不容易出错。
解法 2 是 Copy-Swap 的雏形。C++ 中管理资源类通常会定义自己的 swap 函数,与其他拷贝控制成员(拷贝/移动构造、拷贝/移动赋值运算符、析构)不同,swap 不是必须,但却是重要的优化手段,以下是使用 Copy-Swap 惯用法的解法:
解法 3
  1. class CMyString {
  2.     friend void Swap(CMyString& lhs, CMyString& rhs) noexcept
  3.     {
  4.         // 对 CMyString 的成员逐一交换
  5.         std::swap(lhs.m_pData, rhs.m_pData);
  6.     }
  7.     // ...
  8. };
  9. CMyString(CMyString&& str) : CMyString()
  10. {
  11.     Swap(*this, str);
  12. }
  13. CMyString& operator=(CMyStirng str)
  14. {
  15.     Swap(*this, str);
  16.     return *this;
  17. }
复制代码
这里有几点需要注意:

  • 拷贝赋值运算符的形参类型不再是 const 引用,因为 Copy-Swap 需要先对赋值运算符右侧对象进行拷贝,这里直接使用值传递。这样一来,也使得 Copy-Swap 天然地异常安全、自赋值安全。

    • 异常安全:进入函数 operator=() 之前,先进行拷贝
    • 自赋值安全:形参是一个新创建的临时对象,永远不可能是对象自身

  • 不需要额外实现移动赋值运算符:如果赋值运算符右侧是一个右值,则自动调用 CMyString 的移动构造来构造形参
这还没完...
标准库 std::swap 及 ADL

C++ 标准库也提供了 swap 函数,理论上需要一次拷贝,两次赋值:
  1. void swap(CMyString& lhs, CMyString& rhs)
  2. {
  3.     CMyString tmp(lhs);
  4.     lhs = rhs;
  5.     rhs = tmp;
  6. }
复制代码
其中 CMyString tmp(lhs) 会调用 CMyString 的拷贝构造进行深拷贝,效率上不如 CMyString 类自己实现的直接交换指针的效率高。
在进行  swap(v1, v2) 的调用时,如果类实现了自己的 swap 版本,其匹配程度优于标准库的版本。如果类没有定义自己的 swap,则使用标准库的 swap。这种查找匹配方式被称为 ADL(Argument-Dependent Lookup)。
注意不能使用 std::swap 形式,因为这样会强制使用标准库的 swap。正确的做法是提前使用 using std::swap 声明,而后续所有的 swap 都应该是不加限制的(这一点刚好和 std::move 相反):
  1. void swap(Bar& lhs, Bar& rhs)
  2. {
  3.     using std::swap;
  4.     swap(lhs.m1, rhs.m1);
  5.     swap(lhs.m2, rhs.m2);
  6.     swap(lhs.m3, rhs.m3);
  7. }
复制代码
最终的结果
  1. class CMyString {
  2.     friend void swap(CMyString& lhs, CMyString& rhs) noexcept
  3.     {
  4.         // 对 CMyString 的成员逐一交换
  5.         using std::swap;
  6.         swap(lhs.m_pData, rhs.m_pData);
  7.     }
  8.     // ...
  9. };
  10. CMyString(CMyString&& str) : CMyString()
  11. {
  12.     swap(*this, str);
  13. }
  14. CMyString& operator=(CMyStirng str)
  15. {
  16.     swap(*this, str);
  17.     return *this;
  18. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

我可以不吃啊

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表