C++ / a working model

88 / 103   ·   C++11   ·   约 13 分钟

异常安全:先准备,再提交状态

先记住这句话

RAII 能在离开作用域时回收资源,却不会自动撤销已经写入的业务状态。用配置替换演示如何把可能失败的工作放在局部对象里,确认有效后再交换,让失败路径保持旧配置不变。

本篇内容
  1. 资源安全和状态安全不是一回事
  2. 局部候选对象就是准备区
  3. 把历史机制转为现代实现
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读完整正文

Thinking in C++

两卷作为同一作品记录:已连续阅读Volume1第1—16章、附录A—C、前言和全部章末习题/脚注;Volume2第1—11章、附录A/B、导言、索引和脚注1—162。另逐幅直接阅读两卷HTML所附图像,补齐纯文本中的类图、布局图及递推公式。未执行习题,另售解答、CD录音、外链资料不计入已读范围。完整阅读不表示历史示例符合C++20。

查看版本、实际阅读范围与原文入口 →

资源安全和状态安全不是一回事

两卷之间的一条主线,是从构造与析构管理责任,走向异常传播时仍保持可理解的对象状态。拥有资源的成员能自动清理,但函数若先改了一个字段、随后解析另一个字段失败,析构不会替你恢复前一次写入。

因此先写出接口承诺:配置导入成功就整体替换,失败就保留原配置。本例把输入限定为一组非空名称,不讨论文件系统或跨进程事务。这个边界使我们能够明确分析每个可能抛异常的位置,而不是把异常安全当作所有 C++ 对象天生拥有的属性。

局部候选对象就是准备区

先把新名称逐一放入局部 vector。空名称触发异常,分配失败也可能抛出;这些情况都发生在旧配置尚未触碰时。局部容器负责释放已经准备好的元素,调用者只需决定如何处理失败,而无需知道失败发生在第几个名称。

所有检查完成后再 swap。这里使用默认分配器的 vector,交换不复制字符串,也不需要重新分配元素;提交步骤不会再引入这些失败点。强保证来自这一具体安排,不能推广成“任何两个对象交换都不抛异常”。自定义分配器和自定义 swap 需要单独核查契约。

把历史机制转为现代实现

原书借裸指针、auto_ptr 和手写资源类揭示责任如何丢失。今天保留其所有权思路即可:容器承担序列资源,不为演示而复制旧式引用计数或动态异常规格。auto_ptr 已在 C++17 移除,带类型列表的 throw 规格也不能作为 C++20 接口。

示例验证的是消费者看见的行为:合法输入改变配置,第二项非法时完整旧值不变,空列表可以清空。测试中比较整个序列而非只检查长度,因为保住长度并不代表保住内容。事务对象可能需要暂时多占一份内存,这是这里换取清晰失败保证的明确代价。

容易答错的地方

  • RAII 回收资源不等于回滚业务数据;先修改成员再检查,可能只得到基本保证。
  • swap 的不抛异常性质需要具体类型和分配器条件;不要用本例推导所有 swap 都安全。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
class Config {
    std::vector<std::string> names_;
public:
    const std::vector<std::string>& names() const noexcept { return names_; }
    void replace(const std::vector<std::string>& input) {
        std::vector<std::string> candidate;
        candidate.reserve(input.size());
        for (const auto& name : input) {
            if (name.empty()) throw std::invalid_argument("empty name");
            candidate.push_back(name);
        }
        names_.swap(candidate);
    }
};
int main() {
    Config config;
    config.replace({"alpha", "beta"});
    const auto before = config.names();
    bool rejected = false;
    try { config.replace({"changed", ""}); }
    catch (const std::invalid_argument&) { rejected = true; }
    assert(rejected && config.names() == before);
    config.replace({});
    assert(config.names().empty());
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-thinking-cpp.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

增加名称不可重复的规则,仍保持失败不改变旧配置。

查看参考答案

在 candidate 准备阶段,用 std::find(candidate.begin(), candidate.end(), name) 检查已经加入的名称;命中时抛 invalid_argument,否则 push_back。添加 <algorithm>。输入 {alpha, alpha} 应失败,随后比较 config.names() 与 before 完全相等。交换仍是唯一提交点,查找不修改旧配置;此简单方案为二次复杂度,适合这里的小集合。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录