Effective C++ 条款25:考虑写一个不抛异常的 swap 函数
当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者。对于 classes(而非 templates),要特别特化 std::swap。
一、引言:swap 的重要性被低估了
swap是 C++ 中最简单却最重要的函数之一。它不仅是 STL 排序算法的基石,更是**异常安全编程(Exception-Safe Programming)**的核心工具。
但很多人不知道:默认的std::swap可能对你的类效率极低,而自定义一个不抛异常的swap可以带来质的飞跃。
二、std::swap 的默认实现及其问题
2.1 默认实现
namespacestd{template<typenameT>voidswap(T&a,T&b){Ttemp(a);// 调用拷贝构造a=b;// 调用拷贝赋值b=temp;// 调用拷贝赋值}}2.2 问题场景:Pimpl 惯用法
假设你有一个使用 Pimpl(Pointer to Implementation)的 Widget 类:
classWidgetImpl{public:// 大量数据成员inta,b,c;std::vector<double>data;std::map<std::string,std::string>metadata;// ... 可能成百上千个成员};classWidget{public:Widget(constWidget&rhs);Widget&operator=(constWidget&rhs){*pImpl=*rhs.pImpl;// 深拷贝所有数据!return*this;}~Widget();private:WidgetImpl*pImpl;// 指向实现的指针};使用默认std::swap:
Widget w1,w2;std::swap(w1,w2);// ❌ 三次深拷贝!性能灾难!实际上,我们只需要交换两个指针即可!
三、自定义 swap:三步走策略
3.1 第一步:提供成员 swap
classWidget{public:// ... 其他成员函数// ✅ 成员 swap——高效且不抛异常voidswap(Widget&other)noexcept{usingstd::swap;// 允许 ADLswap(pImpl,other.pImpl);// 只交换指针!}private:WidgetImpl*pImpl;};3.2 第二步:提供 non-member swap
// 在 Widget 的命名空间中namespaceWidgetStuff{classWidget{/* ... */};// ✅ non-member swap——调用成员 swapvoidswap(Widget&a,Widget&b)noexcept{a.swap(b);}}3.3 第三步:特化 std::swap(仅对类,不对类模板)
// ✅ 对具体类特化 std::swapnamespacestd{template<>voidswap<Widget>(Widget&a,Widget&b)noexcept{a.swap(b);}}⚠️重要限制:C++ 标准不允许对函数模板进行偏特化,也不允许在
std命名空间中添加新的模板。因此,对于类模板,我们只能使用 non-member swap 的方式,不能特化std::swap。
四、类模板的 swap 处理
4.1 类模板的情况
namespaceWidgetStuff{// 类模板template<typenameT>classWidgetImpl{// ...};template<typenameT>classWidget{public:voidswap(Widget&other)noexcept{usingstd::swap;swap(pImpl,other.pImpl);}private:WidgetImpl<T>*pImpl;};// ✅ non-member swap 模板——这是推荐做法template<typenameT>voidswap(Widget<T>&a,Widget<T>&b)noexcept{a.swap(b);}}4.2 为什么不能偏特化 std::swap?
// ❌ 错误:C++ 不允许函数模板偏特化namespacestd{template<typenameT>voidswap<Widget<T>>(Widget<T>&a,Widget<T>&b){// 编译错误!a.swap(b);}}// ❌ 错误:不允许在 std 中添加新模板namespacestd{template<typenameT>voidswap(Widget<T>&a,Widget<T>&b){// 未定义行为!a.swap(b);}}五、正确使用 swap 的惯用法
5.1 使用 using std::swap
template<typenameT>voiddoSomething(T&obj1,T&obj2){usingstd::swap;// 引入 std::swapswap(obj1,obj2);// 不加任何命名空间限定!}为什么这样写?
C++ 的名称查找规则(ADL + 常规查找)会按以下优先级选择:
- T 的专属 non-member swap(通过 ADL 找到)
- std::swap 的特化版本
- std::swap 的通用模板
Widget w1,w2;doSomething(w1,w2);// 调用 WidgetStuff::swap(通过 ADL)inta=1,b=2;doSomething(a,b);// 调用 std::swap(内置类型无专属 swap)5.2 异常安全:swap 的强保证
swap不抛异常是实现**强异常安全(Strong Exception Safety)**的关键:
classWidget{public:Widget&operator=(constWidget&rhs){// 基本保证:如果异常发生,对象可能处于中间状态// *pImpl = *rhs.pImpl; // 可能抛异常!// ✅ 强保证:copy-and-swap 惯用法Widgettemp(rhs);// 拷贝可能抛异常,但原对象未改变swap(temp);// swap 不抛异常,安全交换return*this;// temp 在作用域结束时销毁,释放原资源}voidswap(Widget&other)noexcept{usingstd::swap;swap(pImpl,other.pImpl);}private:WidgetImpl*pImpl;};copy-and-swap 的优势:
| 特性 | 直接赋值 | copy-and-swap |
|---|---|---|
| 异常安全 | 基本保证 | 强保证 |
| 自我赋值 | 需要检查 | 自动安全 |
| 代码复杂度 | 中等 | 简单优雅 |
六、实际应用场景
6.1 资源管理类
classResourceManager{public:ResourceManager():resource_(acquireResource()){}~ResourceManager(){releaseResource(resource_);}// 移动操作ResourceManager(ResourceManager&&other)noexcept:resource_(other.resource_){other.resource_=nullptr;}ResourceManager&operator=(ResourceManager&&other)noexcept{if(this!=&other){ResourceManagertemp(std::move(other));swap(temp);}return*this;}// ✅ 不抛异常的 swapvoidswap(ResourceManager&other)noexcept{usingstd::swap;swap(resource_,other.resource_);}private:Resource*resource_;Resource*acquireResource();voidreleaseResource(Resource*r);};// non-member swapvoidswap(ResourceManager&a,ResourceManager&b)noexcept{a.swap(b);}6.2 自定义容器
template<typenameT>classMyVector{public:MyVector():data_(nullptr),size_(0),capacity_(0){}~MyVector(){delete[]data_;}// 移动构造MyVector(MyVector&&other)noexcept:data_(other.data_),size_(other.size_),capacity_(other.capacity_){other.data_=nullptr;other.size_=other.capacity_=0;}// ✅ 不抛异常的 swap——交换三个指针即可voidswap(MyVector&other)noexcept{usingstd::swap;swap(data_,other.data_);swap(size_,other.size_);swap(capacity_,other.capacity_);}// copy-and-swap 赋值MyVector&operator=(MyVector other){// 按值传递,触发拷贝swap(other);// 与临时对象交换return*this;}private:T*data_;size_t size_;size_t capacity_;};// non-member swaptemplate<typenameT>voidswap(MyVector<T>&a,MyVector<T>&b)noexcept{a.swap(b);}6.3 多成员类的 swap
classComplexWidget{public:voidswap(ComplexWidget&other)noexcept{usingstd::swap;// 逐一交换所有成员——全部是不抛异常的操作swap(name_,other.name_);// string 的 swap 不抛异常swap(dimensions_,other.dimensions_);// vector 的 swap 不抛异常swap(metadata_,other.metadata_);// map 的 swap 不抛异常swap(cache_,other.cache_);// unique_ptr 的 swap 不抛异常swap(isVisible_,other.isVisible_);// bool 交换不抛异常}private:std::string name_;std::vector<int>dimensions_;std::map<std::string,std::string>metadata_;std::unique_ptr<Cache>cache_;boolisVisible_;};七、为什么 swap 必须不抛异常?
7.1 异常安全等级
| 等级 | 描述 | swap 的作用 |
|---|---|---|
| 基本保证 | 异常发生后,对象处于有效但不确定状态 | — |
| 强保证 | 异常发生后,对象状态回滚到操作前 | 核心工具 |
| 不抛保证 | 操作绝不抛异常 | swap 本身应达到 |
7.2 swap 不抛异常的原因
voidswap(Widget&a,Widget&b)noexcept{// 只交换指针/内置类型// 这些操作在硬件层面是原子的,不可能失败WidgetImpl*temp=a.pImpl;a.pImpl=b.pImpl;b.pImpl=temp;}swap 通常只涉及:
- 指针交换
- 内置类型交换
- 标准库类型的
swap(已保证不抛异常)
这些操作不可能分配内存,因此不可能抛std::bad_alloc,也没有其他失败条件。
八、现代 C++ 的简化:Rule of Zero
在 C++11 及以后,如果你的类只包含标准库容器、智能指针等自动管理资源类型,遵循 Rule of Zero:
// ✅ Rule of Zero:不需要自定义 swapclassModernWidget{public:// 编译器生成的默认构造、析构、移动、拷贝都正确// std::swap 对这些成员的组合也能高效工作private:std::string name_;std::vector<double>data_;std::unique_ptr<Impl>pImpl_;std::shared_ptr<Cache>cache_;};// 直接使用 std::swap 即可ModernWidget w1,w2;std::swap(w1,w2);// ✅ 高效,因为所有成员都有高效的 swap只有当类包含原始指针或需要手动管理的资源时,才需要自定义 swap。
九、总结
核心原则
- 默认 std::swap 可能效率低下——对于 Pimpl 类尤其如此
- 自定义 swap 应该只交换指针/句柄——避免深拷贝
- swap 必须声明为 noexcept——它是异常安全编程的基石
- 提供成员 swap + non-member swap + std::swap 特化——完整的 swap 支持
实现检查清单
classMyClass{public:// 1. 成员 swap,noexceptvoidswap(MyClass&other)noexcept{usingstd::swap;swap(pImpl_,other.pImpl_);}// 2. copy-and-swap 赋值(可选但推荐)MyClass&operator=(MyClass other){swap(other);return*this;}private:Impl*pImpl_;};// 3. non-member swapvoidswap(MyClass&a,MyClass&b)noexcept{a.swap(b);}// 4. std::swap 特化(仅对非模板类)namespacestd{template<>voidswap<MyClass>(MyClass&a,MyClass&b)noexcept{a.swap(b);}}使用 swap 的正确姿势
template<typenameT>voidgenericFunction(T&a,T&b){usingstd::swap;// 引入标准 swapswap(a,b);// 让编译器选择最佳版本}📌记住:swap 是异常安全编程的瑞士军刀。一个正确实现的 noexcept swap 不仅能让你的类性能飞跃,更能为所有使用它的代码提供强异常安全保证。这是专业 C++ 开发者必须掌握的技术。
参考与延伸阅读
- 《Effective C++》第三版,Scott Meyers,条款25
- 《Exceptional C++》,Herb Sutter,关于异常安全的深入讨论
- CppReference: noexcept specifier
- CppReference: Copy and swap idiom
如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、留言 💬!你的支持是我持续输出的动力!