16.3 异常安全 (Exception Safety)
在开始引入异常处理机制的工程项目中,“异常安全(Exception Safety)” 是每个开发者必须刻在骨子里的准则。
因为 C++ 随时随地可能抛出异常(比如你在做 new 开辟,哪怕是给 vector 只是 push_back 一个元素它也可能因为扩容内存不足而 throw std::bad_alloc 炸开)。
如果一个函数执行到一半,内部调用某条语句暴毙抛出异常被打断了,它是否能保证对象的状态依然完整如初?内存中是否留下了无法被回收的泄漏孤儿? 这就是异常安全的核心拷问。
四个异常安全等级
Section titled “四个异常安全等级”C++ 社区公认,任何带有容错健壮性的函数都必须向调用者承诺以下四个等级中的一个:
1. 不保证异常安全 (No exception safety)
Section titled “1. 不保证异常安全 (No exception safety)”这是最糟糕的代码。如果抛出异常,不仅发生内存泄漏,一些内部对象状态还会残缺损坏(比如类的内部指针悬空)。现代 C++ 绝不允许写出这种级别的产物。
2. 基本保证 (Basic exception safety guarantee)
Section titled “2. 基本保证 (Basic exception safety guarantee)”底线要求。如果发生异常,函数向你起誓:
- 绝对不会有资源内存泄漏。
- 对象仍然处于一个完整的“有效状态”(虽然不知道具体变成了什么样,可能是个空的状态,但是它可以正常安全地过会儿执行析构函数,也可以赋以新值)。
3. 强烈保证 (Strong exception safety guarantee)
Section titled “3. 强烈保证 (Strong exception safety guarantee)”如果发生异常,函数向你起誓:
- 具有类似事务(Transaction)的原子性操作!
- 函数如果在进行中途因意外暴死,对象将会被完好无缺的回滚重置退回到你刚调用这函数时的最初始状态。要么执行得完美成功,要么就像什么事情也从来没有发生过。
4. 绝对不抛异常保证 (No-throw guarantee)
Section titled “4. 绝对不抛异常保证 (No-throw guarantee)”最高圣杯。这函数发誓:我这辈子绝地不会爆任何错抛出任何异常,这事儿我一定能踏实办好!不论你怎么查,绝没意外!
- 所有的内置基本类型(如
int、指针的赋值)、绝大多数类型的析构函数,以及声明了noexcept的那些底层移动操作都拥有这种能力保证。
如何编写异常安全代码?
Section titled “如何编写异常安全代码?”法宝一:RAII 横扫一切内存泄漏
Section titled “法宝一:RAII 横扫一切内存泄漏”利用我们在第 10 章讲过的 RAII 原则,全面使用**智能指针(unique_ptr)**或者专门封装的管理类来代替所有裸指针。
只要没有裸 new 的指针悬挂在那儿,借助上节介绍过的自动“栈展开(Stack Unwinding)”能力,即使被异常从中劈过打断,局部的包装器也会自动被析构从而带走释放掉所有的堆内存!这是能达成「基本保证」的王牌盾牌!
法宝二:Copy and Swap 惯用法
Section titled “法宝二:Copy and Swap 惯用法”如果要修改很多复杂的对象内部,直接在一块上面边改东边边改西边。如果改好了东边,改西边出异常了,此时东边已经被改脏了,这对象就报废处在了中间混乱状态。
“拷贝并交换 (Copy and Swap)” 是业界公认实现「强烈保证」的终结杀招。
它的核心理念是:“不在原件(本体)上做大手术,先拷贝一个全尺寸替身。我们在替身上做手术,如果手术中间失败死了就死了随便扔掉替身。一旦极其危险容易出异常的手术全做确认完妥了。只要执行最后极其快速且能提供 No-throw 保证的一行代码:交换大脑(地址互换)!”
#include <iostream>#include <vector>
class PhotoAlbum {private: std::vector<int> photos; int revision;
public: PhotoAlbum() : revision(0) {}
// 假设我们要对相册进行极其危险的更新操作 // 我们必须提供“强烈保证”:要么成功改完版本+1并存入数据;要么如果加图片失败,连 revision 也不准变! void updateAlbumDangerous(int newPic) { // 第一步:Copy!在一旁偷偷复印一个完整替身相册进行操作 PhotoAlbum tempCopy = *this;
// 第二步:在危险边缘试探。 // push_back 需要动态内存可能抛出 std::bad_alloc 异常致死! tempCopy.photos.push_back(newPic); tempCopy.revision++;
// 此时如果上面的 push 炸了报出异常:没关系!当前这个函数直接死亡, // 局部临时的替身 tempCopy 被栈展开自动安静消灭,它带走了没装进多少的图片垃圾。 // 而 *this 原本体毫发无伤,它的 revision 什么的因为一动也没动,还是最初的模样。这就是回滚!
// 第三步:如果上边惊险通过了,那说明没出异常资源全准备好了。 // 进行 Swap!标准库的 swap(特别是涉及到 vector 转移) 不包含重新分配内存只换核心指针,它是绝对不抛异常(noexcept)的! std::swap(this->photos, tempCopy.photos); std::swap(this->revision, tempCopy.revision);
// 成功换脑离开! }};熟练应用这两种手段,你就能在极其危机的重型金融或底层 C++ 系统中编写出行云流水且万无一失的代码逻辑。