Effective C++ 条款25:考虑写一个不抛异常的 swap 函数
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{templatetypenameTvoidswap(Ta,Tb){Ttemp(a);// 调用拷贝构造ab;// 调用拷贝赋值btemp;// 调用拷贝赋值}}2.2 问题场景Pimpl 惯用法假设你有一个使用 PimplPointer to Implementation的 Widget 类classWidgetImpl{public:// 大量数据成员inta,b,c;std::vectordoubledata;std::mapstd::string,std::stringmetadata;// ... 可能成百上千个成员};classWidget{public:Widget(constWidgetrhs);Widgetoperator(constWidgetrhs){*pImpl*rhs.pImpl;// 深拷贝所有数据return*this;}~Widget();private:WidgetImpl*pImpl;// 指向实现的指针};使用默认std::swapWidget w1,w2;std::swap(w1,w2);// ❌ 三次深拷贝性能灾难实际上我们只需要交换两个指针即可三、自定义 swap三步走策略3.1 第一步提供成员 swapclassWidget{public:// ... 其他成员函数// ✅ 成员 swap——高效且不抛异常voidswap(Widgetother)noexcept{usingstd::swap;// 允许 ADLswap(pImpl,other.pImpl);// 只交换指针}private:WidgetImpl*pImpl;};3.2 第二步提供 non-member swap// 在 Widget 的命名空间中namespaceWidgetStuff{classWidget{/* ... */};// ✅ non-member swap——调用成员 swapvoidswap(Widgeta,Widgetb)noexcept{a.swap(b);}}3.3 第三步特化 std::swap仅对类不对类模板// ✅ 对具体类特化 std::swapnamespacestd{templatevoidswapWidget(Widgeta,Widgetb)noexcept{a.swap(b);}}⚠️重要限制C 标准不允许对函数模板进行偏特化也不允许在std命名空间中添加新的模板。因此对于类模板我们只能使用 non-member swap 的方式不能特化std::swap。四、类模板的 swap 处理4.1 类模板的情况namespaceWidgetStuff{// 类模板templatetypenameTclassWidgetImpl{// ...};templatetypenameTclassWidget{public:voidswap(Widgetother)noexcept{usingstd::swap;swap(pImpl,other.pImpl);}private:WidgetImplT*pImpl;};// ✅ non-member swap 模板——这是推荐做法templatetypenameTvoidswap(WidgetTa,WidgetTb)noexcept{a.swap(b);}}4.2 为什么不能偏特化 std::swap// ❌ 错误C 不允许函数模板偏特化namespacestd{templatetypenameTvoidswapWidgetT(WidgetTa,WidgetTb){// 编译错误a.swap(b);}}// ❌ 错误不允许在 std 中添加新模板namespacestd{templatetypenameTvoidswap(WidgetTa,WidgetTb){// 未定义行为a.swap(b);}}五、正确使用 swap 的惯用法5.1 使用 using std::swaptemplatetypenameTvoiddoSomething(Tobj1,Tobj2){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通过 ADLinta1,b2;doSomething(a,b);// 调用 std::swap内置类型无专属 swap5.2 异常安全swap 的强保证swap不抛异常是实现**强异常安全Strong Exception Safety**的关键classWidget{public:Widgetoperator(constWidgetrhs){// 基本保证如果异常发生对象可能处于中间状态// *pImpl *rhs.pImpl; // 可能抛异常// ✅ 强保证copy-and-swap 惯用法Widgettemp(rhs);// 拷贝可能抛异常但原对象未改变swap(temp);// swap 不抛异常安全交换return*this;// temp 在作用域结束时销毁释放原资源}voidswap(Widgetother)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(ResourceManagerother)noexcept:resource_(other.resource_){other.resource_nullptr;}ResourceManageroperator(ResourceManagerother)noexcept{if(this!other){ResourceManagertemp(std::move(other));swap(temp);}return*this;}// ✅ 不抛异常的 swapvoidswap(ResourceManagerother)noexcept{usingstd::swap;swap(resource_,other.resource_);}private:Resource*resource_;Resource*acquireResource();voidreleaseResource(Resource*r);};// non-member swapvoidswap(ResourceManagera,ResourceManagerb)noexcept{a.swap(b);}6.2 自定义容器templatetypenameTclassMyVector{public:MyVector():data_(nullptr),size_(0),capacity_(0){}~MyVector(){delete[]data_;}// 移动构造MyVector(MyVectorother)noexcept:data_(other.data_),size_(other.size_),capacity_(other.capacity_){other.data_nullptr;other.size_other.capacity_0;}// ✅ 不抛异常的 swap——交换三个指针即可voidswap(MyVectorother)noexcept{usingstd::swap;swap(data_,other.data_);swap(size_,other.size_);swap(capacity_,other.capacity_);}// copy-and-swap 赋值MyVectoroperator(MyVector other){// 按值传递触发拷贝swap(other);// 与临时对象交换return*this;}private:T*data_;size_t size_;size_t capacity_;};// non-member swaptemplatetypenameTvoidswap(MyVectorTa,MyVectorTb)noexcept{a.swap(b);}6.3 多成员类的 swapclassComplexWidget{public:voidswap(ComplexWidgetother)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::vectorintdimensions_;std::mapstd::string,std::stringmetadata_;std::unique_ptrCachecache_;boolisVisible_;};七、为什么 swap 必须不抛异常7.1 异常安全等级等级描述swap 的作用基本保证异常发生后对象处于有效但不确定状态—强保证异常发生后对象状态回滚到操作前核心工具不抛保证操作绝不抛异常swap 本身应达到7.2 swap 不抛异常的原因voidswap(Widgeta,Widgetb)noexcept{// 只交换指针/内置类型// 这些操作在硬件层面是原子的不可能失败WidgetImpl*tempa.pImpl;a.pImplb.pImpl;b.pImpltemp;}swap 通常只涉及指针交换内置类型交换标准库类型的swap已保证不抛异常这些操作不可能分配内存因此不可能抛std::bad_alloc也没有其他失败条件。八、现代 C 的简化Rule of Zero在 C11 及以后如果你的类只包含标准库容器、智能指针等自动管理资源类型遵循 Rule of Zero// ✅ Rule of Zero不需要自定义 swapclassModernWidget{public:// 编译器生成的默认构造、析构、移动、拷贝都正确// std::swap 对这些成员的组合也能高效工作private:std::string name_;std::vectordoubledata_;std::unique_ptrImplpImpl_;std::shared_ptrCachecache_;};// 直接使用 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. 成员 swapnoexceptvoidswap(MyClassother)noexcept{usingstd::swap;swap(pImpl_,other.pImpl_);}// 2. copy-and-swap 赋值可选但推荐MyClassoperator(MyClass other){swap(other);return*this;}private:Impl*pImpl_;};// 3. non-member swapvoidswap(MyClassa,MyClassb)noexcept{a.swap(b);}// 4. std::swap 特化仅对非模板类namespacestd{templatevoidswapMyClass(MyClassa,MyClassb)noexcept{a.swap(b);}}使用 swap 的正确姿势templatetypenameTvoidgenericFunction(Ta,Tb){usingstd::swap;// 引入标准 swapswap(a,b);// 让编译器选择最佳版本}记住swap 是异常安全编程的瑞士军刀。一个正确实现的 noexcept swap 不仅能让你的类性能飞跃更能为所有使用它的代码提供强异常安全保证。这是专业 C 开发者必须掌握的技术。参考与延伸阅读《Effective C》第三版Scott Meyers条款25《Exceptional C》Herb Sutter关于异常安全的深入讨论CppReference: noexcept specifierCppReference: Copy and swap idiom如果这篇文章对你有帮助欢迎点赞 、收藏 ⭐、留言 你的支持是我持续输出的动力