前提:假设所有的析构函数、移动构造和移动赋值、swap函数都具有noexcept保证。

问题:在实践中如何将顺序串联几个具有强异常安全保证的函数组合而成的新函数快速重构成强异常安全的?

例如

void SomeClass::SomeFunc()
{
m_Data1 = f1();
m_Data2 = f2();
m_Data3 = f3();
// f1、f2、f3和对应的operator=都是强异常安全的
}

如何重构实现f3失败时回退f1和f2的操作?


你需要的是事务……

异常安全并不保证整个函数的原子性,后者要求高于前者。

这个改动不是几行代码的事,是业务相关,架构相关的。


来,我教你怎么操作。

void SomeClass::SomeFunc()
{
auto tmp1 = f1();
auto tmp2 = f2();
auto tmp3 = f3();

// critical line, anything below must not throw
using std::swap;
swap(m_Data1, tmp1);
swap(m_Data2, tmp2);
swap(m_Data3, tmp3);
}

总体上Strong exception safety大致就是这种思路,你上面先在临时变数上操作,一通猛于虎之后过一条虚拟的「线」,这条线之下的东西一概不能乱扔果皮纸屑。这条线下面的操作通常就是各种swap或者各种move,所以你给出的前提是必要的,有这些了事情就很好办。而线上面的步骤失败了,你的临时变数自然就被unwound掉,对成员变数等不会造成影响。

这种操作的代价是会有性能开销,特别当swap/move并不很廉价的时候(比如你要操作的是std::array这种),或者你的f函数需要对成员变数同时进行读写(inplace edit变成copy再swap)。所以并不是每个函数都应该strong exception guarantee,很多应用场景下basic guarantee也是可以的。但是没有任何异常安全保证(也就是说你调用的代码哪天给你穿一个异常出来你就开膛破肚漏内存)是绝对不行的,哪怕你的公司是那种禁用异常迅雷不及掩耳盗铃的公司,你也应该全面使用RAII。

PS:举个不需要强异常安全的例子,比如说你处理一个web request,你要操作的对象生命周期与request相同,处理到一半炸异常了你就对外抛一个500之类的出去然后结束请求处理。这时候这个对象炸坏掉就坏掉好了,反正炸了就要析构了。当然,前提是basic exception safety,也就是炸坏了不泄露任何资源。

PS2:也有用这套套路解决不了的情况,比如你的f里面有对某个global/static对象的写操作等副作用,或者更严重的,给外面某个服务发了请求做了什么写入操作,这种没有什么特别通用的好招,前者还可以考虑用个scope guard加反函数来试图消掉,后者估计就上分散式事务吧。这是业务和系统设计层面的事,不是exception safety能解决得了的了。

PS3:这里也引出了一个问题,就是LZ说的要有nothrow move或者swap。我见过一些被某米国军方公司的邪教style guide洗脑洗到丧心病狂的同学写的代码(比如我厂的不少遗产代码),明明是一些个带有data bundle性质的类,非要每个类上来先无脑写宏禁止copy禁止move,然后因为不能copy不能move还要传递出去就裸new、裸指针传得满天飞。看得我内心是崩溃的。你如果有一个field既不能copy又不能move,只能mutate,那自求多福吧,除了让该类的作者写个反函数以外基本没有什么好办法提供强异常安全。所以说早日现代化吧,不要抱著C++98年代的邪教guide当圣旨了,都8021了唉。

——更新——

有同学说解决方案是不使用异常。好!我就在等这个。现在我来简单show一下为什么在这个问题里不使用异常完全是掩耳盗铃,解决不了任何问题。

首先,因为要不使用异常,还要有能够report error、可能会失败的fx三兄弟,于是直接return是不行了auto也用不了了(除非用C++17 std::optional或者boost::optional,如果你这么先进我相信你不会对异常是这个态度,就不多说了)。这里为了简单我们假设三个member的类型叫做T。fx三兄弟要写成:

int f1(T* out);
int f2(T* out);
int f3(T* out);

然后我们来重写题主的someFunc,我们要保持和原版本的语义等价,也就是说如果fx三兄弟有汇报error的,someFunc应该将错误继续上报上去供上层代码处理。

int SomeClass::SomeFunc()
{
int ret = 0;
ret = f1(m_Data1);
if (ret != 0) {
return ret;
}
ret = f2(m_Data2);
if (ret != 0) {
return ret;
}
// ...

已经不用再写下去了。发现什么问题没?如果f2的返回值不是0,发生什么事情了?是不是f1已经把m_Data1修改了,f2一通操作猛于虎,猛一半跪了,整个对象进入了半截状态回不去了?

这和题主原始例子里f2抛出异常有区别么?

没有。

解决方案?简单,还是上面那套,开临时变数,操作,然后swap,做到「Strong errorcode guarantee」。其实等价于Strong exception guarantee,只是错误汇报方式不一样、代码一个简洁一个丑而已。

于是问题来了,你都做到强异常安全了,你还怕异常干什么呀?


赞同 sin1080 。补充一点,对于fx有副作用的时的操作。

当fx有副作用时,它的结果不光体现在局部变数上,那么我们需要有一个反操作,勉强叫reversefx。它和fx同参,做的事情与fx相反。比如

int f1(int n) {
g_some += n; //用全局变数模拟外部操作。
}
int reversef1(int n) {
g_some -= n; //同上
}

这与在银行账户操作中冲正操作类似。

然后利用scope_exit退出时作反向操作即可。


Exception-Safe Coding in C++?

exceptionsafecode.com图标

打开一本资料库事务设计的书来看看?通用的机制不太可能知乎这几页纸说的清楚吧。


应该可以通过改编译器的配置文件

开启异常安全代码通过编译,

开你用什么开发环境或什么编译器

vs这种用的多的应该有解决方案的


谢邀。

将状态和资源申请解耦,延迟状态改变。


不太明白你的意思,怎么回退,执行过的代码还能当没执行过?

手写啊,除非你有时光机,否则不会有通用的办法?


推荐阅读:
查看原文 >>
相关文章