在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_ptr或std::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