强异常安全性指操作要么完全成功,要么状态完全回滚至调用前;它不等于“不抛异常”,而是要求即使中途throw,对象的公开状态、资源和不变量均严格保持原样。

什么是强异常安全性,它和“不抛异常”不是一回事
强异常安全性(Strong Exception Safety)指的是:操作要么完全成功,要么状态完全回滚到调用前——就像什么都没发生过。它不等于“函数不抛异常”,而是要求即使中途 throw,对象的公开状态、资源持有、不变量都必须严格保持原样。
常见误区是以为加个 try/catch 就安全了,其实关键在“回滚时机”和“回滚能力”。比如往 std::vector 插入元素时若内存分配失败,push_back 本身已保证强异常安全;但你自己写的容器若先 new 再复制,就大概率破坏它。
实现强异常安全的核心策略是:**两阶段提交(Commit-then-rollback)**——先在临时空间完成所有可能失败的操作,确认无误后再原子地交换或赋值。
用 copy-and-swap 实现强异常安全的赋值运算符
这是最经典、最易落地的强异常安全模式,适用于大多数自定义类。核心思想是:把“修改本体”这一步放到最后,且确保该步不会抛异常(如 std::swap 对内置类型和多数标准容器都是 noexcept)。
立即学习“C++免费学习笔记(深入)”;
假设你有个管理动态数组的类 MyString:
class MyString {
char* data_;
size_t size_;
public:
MyString& operator=(const MyString& other) {
if (this != &other) {
MyString tmp(other); // 构造函数里可能 throw(如 new 失败),但不影响 *this
swap(tmp); // swap 必须是 noexcept,且只交换指针,不抛异常
}
return *this;
}
<pre class="brush:php;toolbar:false;">void swap(MyString& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}};
- 构造
tmp是“准备阶段”,失败则*this完全未改动 -
swap是“提交阶段”,必须标记为noexcept,否则编译器无法认定强异常安全 - 别直接在
operator=里delete原数据再new—— 中途抛异常会导致悬空指针或内存泄漏
手动 Commit/rollback 模式:适用于不可拷贝或开销敏感场景
当对象太大、不可拷贝(如含唯一资源句柄),或你想精确控制回滚逻辑时,就得自己管理“预备→提交→回滚”三步。典型结构是:申请资源 → 执行变更 → 成功则 commit(移交所有权),失败则 rollback(释放临时资源)。
例如向一个带锁的容器插入元素:
void Container::insert(const Item& item) {
auto new_node = std::make_unique<Node>(item); // 可能 throw(构造或分配)
std::lock_guard<std::mutex> lock(mutex_);
<pre class="brush:php;toolbar:false;">// 此处做实际链表插入逻辑(假设不会 throw)
new_node->next_ = head_;
head_ = new_node.release(); // release 不 throw,是真正的 commit 点
// 如果上面 release 前抛异常,new_node 析构自动 delete,无泄漏}
-
std::make_unique是预备动作,失败则全程无副作用 -
release()是唯一 commit 点,它只是转移裸指针,不涉及内存分配或构造,因此不会抛异常 - 别写成
head_ = new Node(item)——new和赋值分两步,中间若new成功但赋值前抛异常,Node就泄漏了 - 注意:
std::lock_guard的作用域要覆盖整个操作,避免锁提前释放导致竞态
容易被忽略的坑:析构函数不能 throw,但 RAII 对象的析构可能间接触发异常
强异常安全的前提是:所有清理路径(尤其是析构函数)都必须是 noexcept。C++11 起,析构函数默认是 noexcept,但如果你显式写了 throw() 或调用了可能 throw 的函数,就会违反规则。
常见雷区:
- 在析构函数里调用
close()文件描述符,而close在某些系统上可能因内核错误返回 -1 并设置errno—— 这不算 throw,但若封装成抛异常的 wrapper 就危险了 - RAII 类中持有另一个可能 throw 析构的类(极少见,但模板嵌套时容易误传)
- 使用
std::shared_ptr管理资源时,其自定义删除器如果 throw,会导致程序终止(因为析构函数隐式noexcept)
真正难调试的是“异常途中又抛异常”:比如你在 catch 块里试图 rollback,结果 rollback 过程又 throw —— C++ 会直接调用 std::terminate。所以 rollback 逻辑本身必须 100% 不抛异常,哪怕只是记录日志也得用 noexcept 版本。


















