Skip to content

异常安全 ​

之前说过了try-catch模型, 那么异常安全实际上在思考一件事情 —— 在发生了异常之后, 整个系统现在是什么状态

此处一般可以分成四个等级 ——

保证级别异常发生后保证什么
No guarantee不承诺异常之后系统会处于什么状态
Basic guarantee对象仍然合法、不会泄漏资源,但状态可能已经改变
Strong guarantee操作要么完全成功,要么像完全没发生过
No-throw guarantee保证根本不会抛异常

资源状态比较容易通过 RAII 等方式进行保障至少不会资源泄露. Strong Guarantee 比较有意思, 重点说说

一个例子 ​

C++
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 了.

而只要简单的交换一下语序, 即可 ——

C++
void AddItem(Item item)
{
	items.push_back(item);
	item_count++;
}

这样在 push_back 抛出异常的时候, 我们的 item_count 还是旧的状态没有改变, 则在外部看来我们确乎是添加 Item 的操作没有实现.

那么这边就涉及到了一个非常重要的异常安全设计原则 —— 先做可能抛出异常的操作, 最后再执行不会抛出异常的提交

Commit / Swap ​

一个简单的模型, 我们可以把一个操作划分成 ——

  1. Commit 之前: 允许失败, 但是不对原状态进行修改
  2. Commit 操作: 不允许抛出异常, 必须是 noexcept 操作
  3. Commit 之后: 也不允许失败.

简单的例子 ——

C++
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 ​

C++
void Registry::Add(Item item)
{
    _items.push_back(item);
    _lookup.emplace(item.id, items_.size() - 1);
}

假设lookup.emplace抛出异常了, 但是 item 已经被添加, 目前它的状态是 id 错误, 但是已经被添加. 这个函数没有保证 strong guarantee.

当然我们也可以清澈一点这样写 ——

C++
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 吧. —— 不过实际上已经加入实验了, 不赖

C++
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();
}

异常安全组合 ​

某一个操作假设是强安全保证, 但是不能保证整体是强安全保证.

C++
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 的影响非常大, 在分析的时候注意点吧.

Released under the MIT License.