异常安全
之前说过了try-catch模型, 那么异常安全实际上在思考一件事情 —— 在发生了异常之后, 整个系统现在是什么状态
此处一般可以分成四个等级 ——
| 保证级别 | 异常发生后保证什么 |
|---|---|
| No guarantee | 不承诺异常之后系统会处于什么状态 |
| Basic guarantee | 对象仍然合法、不会泄漏资源,但状态可能已经改变 |
| Strong guarantee | 操作要么完全成功,要么像完全没发生过 |
| No-throw guarantee | 保证根本不会抛异常 |
资源状态比较容易通过 RAII 等方式进行保障至少不会资源泄露. Strong Guarantee 比较有意思, 重点说说
一个例子
void AddItem(Item item)
{
item_count++; // no except
items.push_back(item); // 本身提供 Strong Guarantee
}
const int GetItemCount() const
{
return item_count;
}逻辑非常简单, 我们要往容器里面添加物品, 那么物品计数增加, 同时往容器中添加物品.
假设此处 push_back 操作抛出异常了, 但是 item_count++ 却已经执行, 说明在外部看来其实我们已经完成了添加, 这就不是 Strong Guarantee 了.
而只要简单的交换一下语序, 即可 ——
void AddItem(Item item)
{
items.push_back(item);
item_count++;
}这样在 push_back 抛出异常的时候, 我们的 item_count 还是旧的状态没有改变, 则在外部看来我们确乎是添加 Item 的操作没有实现.
那么这边就涉及到了一个非常重要的异常安全设计原则 —— 先做可能抛出异常的操作, 最后再执行不会抛出异常的提交
Commit / Swap
一个简单的模型, 我们可以把一个操作划分成 ——
- Commit 之前: 允许失败, 但是不对原状态进行修改
- Commit 操作: 不允许抛出异常, 必须是 noexcept 操作
- Commit 之后: 也不允许失败.
简单的例子 ——
void SetConfig(Config newConfig)
{
Validate(newConfig); // 可能抛出异常
Prepare(newConfig); // 可能抛出异常
static_assert(std::is_nothrow_move_assignable_v<Config>);
_config = std::move(newConfig); // commit 操作, 不允许 throw
}或者使用 std::swap(_config, newConfig) 来代替赋值, 也是类似的原理 —— 不过 Compare-And-Swap 很神圣, 在很多地方都会遇到这种思想
(那么双缓冲好像也有点像是这样, 我们只有正确执行了所有操作之后, 最后再刷入缓冲, 如果有异常则不刷)
Scope Rollback
void Registry::Add(Item item)
{
_items.push_back(item);
_lookup.emplace(item.id, items_.size() - 1);
}假设lookup.emplace抛出异常了, 但是 item 已经被添加, 目前它的状态是 id 错误, 但是已经被添加. 这个函数没有保证 strong guarantee.
当然我们也可以清澈一点这样写 ——
void Registry::Add(Item item)
{
_items.push_back(item);
try
{
_lookup.emplace(item.id, items_.size() - 1);
}
catch (...)
{
_items.pop_back();
throw;
}
}那么如果要操作的对象多了, 这个方法也会不断膨胀, 很坏(
所以就委托给 rollback(注意, rollback 要求 noexcept), 把需要撤销的方法全部给它, 然后用一个 RAII 管理器来进行封装, 这样在抛出异常的时候 stack unwinding 会帮助我们执行从而撤销状态. 比较妙. 可以参考或者使用 Boost.Scope 吧. —— 不过实际上已经加入实验了, 不赖
void Registry::Add(Item item)
{
_items.push_back(item);
auto rollback = std::experimental::scope_exit([&] noexcept {
_items.pop_back();
});
_lookup.emplace(
item.id,
_items.size() - 1
);
rollback.release();
}异常安全组合
某一个操作假设是强安全保证, 但是不能保证整体是强安全保证.
void Foo()
{
StrongGuarantee_OP();
BasicGuarantee_OP();
NoGuarantee_OP();
}
void Foo()
{
StrongGuarantee_A();
StrongGuarantee_B();
}那么 Foo 方法依旧不是 Strong Guarantee. 比较反直觉(到也还行, 类比一下原子操作, 或者说看下最开始引入到例子也比较类似), 局部 Strong Guarantee 不会自动组合成整体 Strong Guarantee.
总结
Strong Guarantee 确乎很帅, 但是也带来了额外的空间, 心智负担等, 所以看业务是否需要吧. 不过确乎是有意思的. 当然, 其实在实践当中也会发现, 我们调用的外部方法产生的副作用对 Strong Guarantee 的影响非常大, 在分析的时候注意点吧.