Item 25:考虑写出一个不抛异常的 swap
对齐 Effective C++ 第三版 Item 25:为 pimpl 与资源类型实现高效 noexcept member swap、同命名空间 non-member swap、正确的 ADL 调用协议及非模板类型的 std::swap 特化。
学习目标
- 能解释通用 std::swap 与 pimpl member swap 的成本差异,并实现 O(1) noexcept 交换
- 能写出同 namespace non-member swap 和泛型
using std::swap; swap(a,b)查找协议 - 能判断 std::swap 全特化、class template、allocator 与条件 noexcept 的合法边界
从一个昂贵的通用交换开始
std::swap(a, b) 的通用实现概念上需要一个临时对象和若干 move/copy assignments:
template<class T>
void genericSwap(T& a, T& b) {
T temp(std::move(a));
a = std::move(b);
b = std::move(temp);
}对普通 value type 可能足够;对拥有昂贵 invariant、custom allocation 或 pimpl 的类型,它可能做远多于“交换身份”所需的工作。
std::swap fallback
临时对象 → 两次 assignment;可能分配、复制,异常也可能发生在中途。
pimpl member swap
只交换两个 unique owner,O(1)、不分配,并把交换变成可靠的 non-throwing swap。
Item 25 的原则是 Consider support for a non-throwing swap(考虑写出一个不抛异常的 swap)。目标包含两部分:找到类型最便宜的表示交换,并把它接入泛型查找协议。
↡在不分配、不复制业务资源且不会抛异常的情况下交换两个对象的可观察值。pimpl 只需要交换 implementation owner
pimpl idiom(pimpl)把大部分表示放在独立 implementation object:
class Widget {
public:
Widget();
Widget(const Widget& rhs);
Widget& operator=(Widget rhs);
~Widget();
void swap(Widget& other) noexcept;
private:
std::unique_ptr<WidgetImpl> pImpl_;
};Widget 的全部状态由 pImpl_ owner 表示。交换两个 Widget 无需复制 WidgetImpl,只交换两个 unique owners:
void Widget::swap(Widget& other) noexcept {
pImpl_.swap(other.pImpl_);
}unique_ptr swap 是常数时间且不分配,因此 Widget member swap 可正确声明 noexcept。
为什么 swap 应尽量不抛
swap 常被用作事务的 commit point:先在临时对象中完成可能失败的准备,再一次交换到目标。
↡所有可能失败的工作成功后,以一个不可失败操作发布完整新状态的边界。Widget& Widget::operator=(Widget rhs) {
swap(rhs);
return *this;
}按值参数 rhs 在进入函数体前完成 copy/move;若失败,*this 未变。函数体 swap 若不抛,新状态一次提交,旧状态随 rhs 析构。
若 swap 在交换一半成员后抛异常,目标和候选都可能部分改变,commit 模型失效。
noexcept 必须来自成员保证
不能为了满足建议而撒谎声明 noexcept。若 member swap 可能抛,noexcept 会在异常逃出时调用 std::terminate。
成员组合类型应使用条件 noexcept:
class Record {
public:
void swap(Record& other)
noexcept(noexcept(std::swap(id_, other.id_)) &&
noexcept(std::swap(payload_, other.payload_)));
private:
Id id_;
Payload payload_;
};实现中使用成员的高效 swap;对于未知泛型成员,应采用 ADL-aware helper,而不是硬编码所有 std::swap。
同 namespace non-member swap 接入 ADL
member swap 高效但泛型算法通常调用 free swap(a,b)。因此在类型 namespace 提供 non-member overload:
namespace ui {
void swap(Widget& lhs, Widget& rhs) noexcept {
lhs.swap(rhs);
}
} // namespace ui它不需要 friend,因为 member swap 已是 public;它只是连接查找协议与 private-aware implementation。
↡public member 提供表示交换,non-member overload 提供泛型发现的双层定制结构。泛型代码不要直接限定 std::swap
正确的泛型调用模式是:
template<class T>
void exchangeValues(T& a, T& b)
noexcept(noexcept(swap(a, b)))
{
using std::swap;
swap(a, b);
}先 using std::swap 引入标准 fallback,再进行 unqualified call,让 ADL 同时寻找类型 namespace 的专用 overload。
using std::swap先保留标准 fallback
swap(a, b)类型 namespace 的 overload 可进入候选
ui::swap → member swap直接交换 private representation
std::swap fallback仍有可用的通用后备路径
直接写 std::swap(a,b) 会绕过普通 ADL overload;只写 swap(a,b) 又可能在没有定制时找不到 fallback。
非模板用户类型可选 std::swap 全特化
对具体 user-defined type,可以为 std::swap 提供符合规则的 explicit full specialization,转调 member swap:
namespace std {
template<>
void swap(ui::Widget& lhs, ui::Widget& rhs) noexcept {
lhs.swap(rhs);
}
} // namespace std这使显式写 std::swap(widgetA, widgetB) 的代码也走高效路径。但首选仍是同 namespace non-member overload 加 ADL-aware 调用,因为覆盖面更自然。
不得向 std 随意添加普通 overload;只能在标准允许的 user-defined type specialization 边界内操作。
class template 不要试图偏特化函数模板
若 Widget 本身是 class template:
template<class T>
class Widget;希望为所有 Widget<T> 偏特化 std::swap 会遇到问题:function templates 不能 partial specialization。
正确做法是在 Widget 自己的 namespace 提供 function template overload:
namespace ui {
template<class T>
void swap(Widget<T>& lhs, Widget<T>& rhs)
noexcept(noexcept(lhs.swap(rhs)))
{
lhs.swap(rhs);
}
} // namespace ui不要在 std namespace 添加该 overload。
member swap 应逐项使用正确 swap
非 pimpl 类型可能有多个成员。member swap 要为每个成员选择最合适定制:
void Document::swap(Document& other)
noexcept(noexcept(swap(title_, other.title_)) &&
noexcept(swap(body_, other.body_)))
{
using std::swap;
swap(title_, other.title_);
swap(body_, other.body_);
}实际代码中 noexcept expression 的 name lookup 要保证声明顺序和 helper 可见;可用 std::is_nothrow_swappable_v 等 traits 简化条件。
若任一成员 swap 会抛,外层不能承诺无条件 noexcept。
allocator-aware 类型要审查交换前提
标准容器和自定义 allocator-aware 类型的 swap 可能受 allocator equality 与 propagation traits 约束。交换内部 buffer owner 前,必须确保释放责任仍匹配 allocator。
↡容器 swap 时 allocator 是否随对象一起交换的类型属性。propagate_on_container_swap == true
-> allocator 与 storage 一起交换
false and allocators equal
-> storage 可安全交换
false and allocators differ
-> 不能盲目交换 owner这类 swap 的 noexcept condition 必须与 allocator contract 一致,不能只看 pointer swap。
swap 还影响容器和算法优化
标准算法、sort、rotate、optional/variant 和容器操作会依据 swappability/noexcept 选择路径或保证。正确 swap 不只是赋值实现细节。
↡泛型算法通过 swap traits 和 ADL 使用用户类型高效交换能力的集成契约。若 move construction 抛但 swap 不抛,算法仍可能用 swap 维持更强保证。反之,错误 noexcept 会把可恢复异常变成 terminate。
↡算法根据操作的 noexcept 属性选择能维持性能或异常保证的实现分支。用失败注入和查找轨迹验收
先预测每种调用形式最终选择哪个 function,再建立验证矩阵:
- member
a.swap(b)、ADLswap(a,b)、显式std::swap(a,b)分别记录命中路径。 - pimpl swap 验证 implementation address 互换、无 allocation/copy/move。
- static assertions 验证
is_nothrow_swappable与函数声明一致。 - copy-and-swap failure injection:candidate 构造失败时目标不变,commit 不抛。
- class template 实例验证同 namespace overload,不向
std添加非法 overload。 - allocator 相等/不等与 propagation 组合验证 owner/deallocator 配对。
- self-swap 保持合法值,资源 ledger 不 double free 或泄漏。
只有查找正确、成本正确、noexcept 真实三者同时成立,swap 定制才完成。
先预测 pimpl、无定制类型和 allocator 不匹配三种样本的路径,再用实验切换它们:
Lab
先预测:这次 swap 会走哪条路径?
切换样本,观察查找入口、资源 owner 和 noexcept 结论如何一起变化。
member swap 只交换 implementation owner
resolution trace
ui::swap → Widget::swap → unique_ptr::swap交换身份,不搬运 WidgetImpl;适合作为 copy-and-swap 的 commit point。
判定
通过:O(1),不分配,noexcept 可以由成员保证支撑。
当前样本:pimpl owner;记录查找轨迹、owner 变化与 noexcept 前提,切换后可用重置回到 pimpl 基线。
小结
- pimpl/resource type 应提供直接交换 representation 的 member swap,避免通用 move/copy 成本
- non-throwing swap 可作为 prepare-then-commit 的可靠提交点,支撑 strong exception guarantee
- 在类型 namespace 提供 non-member swap 转调 member,使 ADL 能发现定制
- generic code 使用
using std::swap; swap(a,b),同时保留用户定制与标准 fallback - 具体非模板 user type 可选 std::swap 全特化;class template 用自身 namespace function template overload
- noexcept 必须由 member、allocator 和递归 swap 的真实保证推导,不能虚假声明
名词解释
本章出现的专业名词,用大白话再讲一遍。
- generic swap fallback
通过 move/copy 构造赋值交换完整对象的后备实现。
- non-throwing swap
不分配且不会抛异常的值交换。
- pimpl idiom
public class 以 owner 隐藏 implementation 表示。
- member swap
直接交换 private representation 的成员函数。
- commit point
所有准备成功后发布状态的不可失败边界。
- strong exception guarantee
失败保持原值,成功发布完整结果。
- noexcept termination rule
异常逃出 noexcept 函数会终止程序。
- conditional noexcept
由组成操作编译期推导是否不抛。
- ADL-visible swap
同类型 namespace、可由 ADL 发现的 swap。
- two-layer swap customization
member 实现交换、non-member 提供发现。
- unqualified swap call
不带 namespace 限定并允许 ADL 的调用。
- std swap fallback
无用户定制时的标准通用交换。
- std::swap specialization
具体用户类型的标准 swap 全特化。
- explicit full specialization
所有模板参数已确定的单一特化。
- function-template partial specialization
函数模板不支持的部分参数特化机制。
- template swap overload
同 class template namespace 的 swap function template。
- compositional swap
递归为成员使用 ADL-aware swap 的实现。
- nothrow swappable trait
判断类型交换是否存在且不抛的性质。
- allocator propagation policy
swap 是否同时交换 allocator 的策略。
- allocator ownership invariant
allocation 必须由兼容 allocator 释放。
- generic swappability contract
用户类型与泛型算法的交换集成约定。
- noexcept-driven algorithm selection
算法按 noexcept 选择实现路径。
- swap resolution trace
记录查找、交换、异常与 owner 变化的轨迹。
练习
- 问题 1:为 pimpl Widget 定制 swap。 写出 member、namespace overload 和 generic call,并说明每层职责。
- 问题 2:处理 Widget template。 团队试图为所有
Widget<T>偏特化std::swap,请修正并验证。
- 问题 3:审查 allocator-aware Buffer。 两对象 allocator 不同且不传播,现有 noexcept swap 只交换 pointers,请分析风险。