导读:本期聚焦于小伙伴创作的《怎样编写异常安全的C++代码?强异常安全保证的实现方法有哪些》,敬请观看详情。当函数执行到一半抛出异常,对象状态却已经改变,后续逻辑就可能读到脏数据。强异常安全保证要求操作要么完全成功要么无任何副作用,这依赖资源管理的确定性。C++没有垃圾回收,裸指针和手动释放极易在异常路径上泄漏内存或产生半成品状态。借助RAII把资源绑定到对象生命周期,再用拷贝再交换手法隔离变更,可以让函数在抛异常时回滚到调用前状态。标准库容器大多提供基本或强保证,自定义类型则需显式设计。下文从原理到代码演示如何落地强保证,并比较不同方案的代价与适用场景。

在C++程序里,异常可能从任何抛出点中断控制流。如果一段代码修改了多个对象或释放了旧资源,却在提交前抛异常,程序就会停留在不一致状态。强异常安全保证的核心目标是:调用前后,程序所有可观察状态不变,要么全做要么不做。实现它并不靠运气,而是靠几条明确的工程规则。

怎样编写异常安全的C++代码?强异常安全保证的实现方法有哪些

什么是强异常安全保证

异常安全有三个常见等级。基本保证指异常发生后对象仍可用,没有泄漏,但状态可能变化。弱保证(通常也叫基本保证的更强形式)指不变量维持,但具体值不确定。强保证则最严格:如果函数因异常退出,程序状态等价于函数从未被调用。对于银行转账、配置替换、容器重排这类操作,强保证几乎是必须的。

标准库中有不少强保证的例子。比如std::vector::push_back在扩容时若分配失败,原vector内容不变;std::swap对多数类型提供 noexcept 交换,因此基于交换的提交可视为不抛操作。理解这些案例有助于我们在自己类型上复用同样思路。

用RAII管理资源是前提

强保证的第一步是把每一份资源(内存、文件句柄、锁)交给一个栈对象,在其析构函数里释放。这样无论函数从哪条路径退出,栈展开都会自动清理。若手动写 delete 或 close,一旦前面某行抛异常,后面的清理就被跳过。

下面这段代码没有RAII,在异常下会泄漏:

#include <iostream>

void bad_example() {
    int* p = new int[10];
    // 如果下面这行抛异常,p不会被释放
    throw std::runtime_error("oops");
    delete[] p;
}

改用std::unique_ptrstd::vector后,析构自动发生,异常安全等级立刻提升:

#include <memory>
#include <stdexcept>

void good_example() {
    auto p = std::make_unique<int[]>(10);
    // 即便抛异常,unique_ptr析构也会释放数组
    throw std::runtime_error("oops");
}

拷贝再交换(copy and swap)手法

实现强保证最常用的 idiom 是 copy and swap:先在局部构造一份新状态的副本,所有可能抛异常的操作都作用在副本上;全部成功后再用不抛异常的 swap 把副本和当前对象交换。由于 swap 通常不分配内存,可以标记为 noexcept,提交步骤绝对安全。

下面的类用此手法提供强保证的赋值运算符:

#include <vector>
#include <algorithm>

class Buffer {
public:
    Buffer(std::size_t n) : data_(n) {}

    // 强异常安全赋值
    Buffer& operator=(const Buffer& other) {
        // 1. 拷贝构造可能抛,但只影响临时对象
        Buffer tmp(other);
        // 2. swap 不抛,完成提交
        swap(tmp);
        return *this;
    }

    void swap(Buffer& other) noexcept {
        using std::swap;
        swap(data_, other.data_);
    }

private:
    std::vector<int> data_;
};

上面的operator=中,若Buffer tmp(other)抛异常,当前对象完全没动过;只有拷贝成功后才交换,因此满足强保证。注意 swap 必须 noexcept,否则编译器在栈展开时若再抛会直接 terminate。

同样手法可用于成员函数。例如替换内部配置:

#include <string>
#include <vector>

class Service {
public:
    void update_config(const std::vector<std::string>& cfg) {
        auto tmp = cfg;          // 拷贝,可能抛
        tmp.swap(config_);       // 不抛提交
    }

private:
    std::vector<std::string> config_;
};

事务化多步操作

当函数需要改动多个独立对象,且要求整体原子性时,可以为每个对象准备副本,全部准备就绪后再统一交换。若其中一步失败,已准备好的副本随栈退出自动销毁,原对象纹丝不动。

假设我们要同时更新两个关联表:

#include <map>
#include <string>

class Store {
public:
    void transfer(const std::string& from, const std::string& to, int amount) {
        // 拷贝两份账本
        auto a = accounts_;
        auto b = logs_;
        // 修改副本,可能抛
        a[from] -= amount;
        a[to] += amount;
        b.push_back(from + "->" + to);
        // 全部成功后提交
        accounts_.swap(a);
        logs_.swap(b);
    }

private:
    std::map<std::string, int> accounts_;
    std::vector<std::string> logs_;
};

这种写法在副本构造和修改阶段完全不碰原数据,只有最后两步 swap 是安全点。代价是额外内存和拷贝时间,所以在性能敏感且 rarely 失败的场景要权衡。

与标准库设施的配合

标准容器自身提供不同级别保证。例如std::vector::reserve在失败时不改变原vector;std::map::insert若因比较函数抛异常,树结构保持不变。编写自定义容器时,应参考这些契约,在文档中写明保证级别。

另一个关键点是避免析构函数抛异常。析构若抛且正处栈展开,程序直接终止。因此 RAII 对象的释放动作应吞掉或转记错误,而非抛出。这保证了强保证的回滚路径本身不会二次失败。

方案异常安全级别额外开销适用场景
裸指针手动管理不推荐
RAII基础管理基本保证单一资源
拷贝再交换强保证中(拷贝成本)状态替换
事务多副本强保证高(多副本)跨对象原子

小结与实践建议

编写强异常安全代码不是给每个函数加 try-catch,而是从资源归属和提交方式上设计。优先用智能指针和容器接管资源;对需要原子变更的类型实现 noexcept 的 swap,并以拷贝再交换组织逻辑;跨对象操作时采用事务式副本。最后用单元测试故意抛异常(如重载 new 失败)验证状态回滚,才能确信保证真实有效。

exception_safetyRAIIcopy_and_swap修改时间:2026-08-11 06:24:33

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。