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。

交换表示,而不是搬运整个对象:这是 pimpl 类型值得提供 member 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。

泛型代码不要直接限定 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。

generic callusing std::swap

先保留标准 fallback

ordinary lookup + ADLswap(a, b)

类型 namespace 的 overload 可进入候选

user customizationui::swap → member swap

直接交换 private representation

no customizationstd::swap fallback

仍有可用的通用后备路径

非限定调用同时保留标准后备与 ADL 定制;直接写 std::swap 会跳过这条发现路径。

直接写 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。

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 不只是赋值实现细节。

若 move construction 抛但 swap 不抛,算法仍可能用 swap 维持更强保证。反之,错误 noexcept 会把可恢复异常变成 terminate。

用失败注入和查找轨迹验收

先预测每种调用形式最终选择哪个 function,再建立验证矩阵:

  • member a.swap(b)、ADL swap(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 ownerowner / lookup / exception contract

当前样本: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 的真实保证推导,不能虚假声明

资料与写作方式声明

本章以Effective C++, Third Edition, Item 25权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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. 问题 1:为 pimpl Widget 定制 swap。 写出 member、namespace overload 和 generic call,并说明每层职责。
  1. 问题 2:处理 Widget template。 团队试图为所有 Widget<T> 偏特化 std::swap,请修正并验证。
  1. 问题 3:审查 allocator-aware Buffer。 两对象 allocator 不同且不传播,现有 noexcept swap 只交换 pointers,请分析风险。

讨论

评论区加载中…