什么是Copy-and-Swap惯用法,以及为什么它在2025年是必备技能
想象一下,您正在编写一个处理动态内存的类。一切进展顺利,直到需要重载赋值运算符。您编写operator=,检查自赋值,手动释放旧资源,分配新资源……然后在90%的情况下,您会引入一个导致内存泄漏或双重释放的错误。熟悉吗?
在C++中,有一个优雅的解决方案——copy-and-swap惯用法。这不仅仅是一个花哨的技巧,而是一项基础技术,它允许您编写安全、异常安全且简洁的代码。在2025年,当C++23已被广泛使用,而C++26标准即将到来之际,这个惯用法仍然是管理资源的类的黄金标准。
在本文中,我们将从头到尾剖析copy-and-swap惯用法:从它解决的问题开始,到实际的示例和2025年的最佳实践。准备好您的编译器——会有很多代码!
问题:为什么天真的operator=是有害的
让我们从一个管理动态数组的经典类示例开始。没有使用copy-and-swap惯用法,代码通常看起来像这样:
class DumbArray {public: DumbArray(size_t size) : size_(size), data_(new int[size]) {} ~DumbArray() { delete[] data_; } // 天真的赋值运算符 DumbArray& operator=(const DumbArray& other) { if (this == &other) return *this; // 检查自赋值 delete[] data_; // 释放旧资源 size_ = other.size_; data_ = new int[size_]; // 分配新资源 std::copy(other.data_, other.data_ + size_, data_); return *this; } private: size_t size_; int* data_;};乍一看,一切都很合理。但存在三个问题:
- 缺乏基本异常保证。 如果
new int[size_]抛出std::bad_alloc,对象将处于无效状态:data_将指向已释放的内存。 - 逻辑重复。 数据复制已在拷贝构造函数中实现。为什么我们要手动编写相同的代码?
- 自赋值检查。 它拖慢了代码速度,并且很容易被遗忘。此外,在95%的情况下不会发生自赋值,但检查仍然会执行。
这还不是全部。如果向类中添加其他资源(例如文件描述符或互斥锁),代码不仅会变得臃肿——还会变得危险。正是在这里,copy-and-swap惯用法登场了。
惯用法的核心:实现完美operator=的三个步骤
Copy-and-swap惯用法基于一个简单的想法:不要在赋值运算符中编写复制逻辑——使用拷贝构造函数和swap函数。算法如下:
- Copy(复制): 使用拷贝构造函数创建对象的临时副本。
- Swap(交换): 将当前对象的内容与临时副本交换。
- Destroy(销毁): 临时对象被销毁,释放旧资源。
在代码中,它看起来像这样:
class SmartArray {public: SmartArray(size_t size = 0) : size_(size), data_(size ? new int[size] : nullptr) {} // 拷贝构造函数 SmartArray(const SmartArray& other) : size_(other.size_), data_(other.size_ ? new int[other.size_] : nullptr) { std::copy(other.data_, other.data_ + size_, data_); } // 析构函数 ~SmartArray() { delete[] data_; } // 友元swap函数 friend void swap(SmartArray& first, SmartArray& second) noexcept { using std::swap; swap(first.size_, second.siz