如何编写异常安详的C++代码
副标题#e#
关于C++中异常的争论何其多也,但往往是一些不合事实的误解。异常曾经是一个难以用好的语言特性,幸运的是,跟着C++社区履历的积聚,本日我们已经有足够的常识轻松编写异常安详的代码了,并且编写异常安详的代码一般也不会对机能造成影响。
利用异常照旧返回错误码?这是个争论不休的话题。各人必然传闻过这样的说法:只有在真正异常的时候,才利用异常。那什么是“真正异常的时候”?在答复这个问题以前,让我们先看一看措施设计中的稳定式道理。
工具就是属性聚合加要领,如何鉴定一个工具的属性聚合是不是处于逻辑上正确的状态呢?这可以通过一系列的断言,最后下一个结论说:这个工具的属性聚合逻辑上是正确的可能是有问题的。这些断言就是权衡工具属性聚合对错的稳定式。
我们凡是在函数挪用中,实施稳定式的查抄。稳定式分为三类:前条件,后条件和稳定式。前条件是指在函数挪用之前,必需满意的逻辑条件,后条件是函数挪用后必需满意的逻辑条件,稳定式则是整个函数执行中都必需满意的条件。在我们的接头中,稳定式既是前条件又是后条件。前条件是必需满意的,假如不满意,那就是措施逻辑错误,后条件则不必然。此刻,我们可以用稳定式来严格界说异常状况了:满意前条件,可是无法满意后条件,即为异常状况。当且仅当产生异常状况时,才抛出异常。
关于何时抛出异常的答复中,并不排出返回值陈诉错误,并且这两者是正交的。然而,从我们履历上来说,完全可以在这两者中加以选择,这又是为什么呢?事实上,当我们做出这种选择时,一定意味着接口语意的改变,在不改变接口的环境下,其实是无法选择的(试试看,用返回值处理惩罚结构函数中的错误)。通过稳定式区别出正常和异常状况,还可以更好地提炼接口。
#p#副标题#e#
对付异常安详的评定,可分为三个级别:根基担保、强担保和不会失败。
根基担保:确保呈现异常时措施(工具)处于未知但有效的状态。所谓有效,即工具的稳定式查抄全部通过。
强担保:确保操纵的事务性,要么乐成,措施处于方针状态,要么不产生改变。
不会失败:对付大大都函数来说,这是很难担保的。对付C++措施,至少析构函数、释放函数和swap函数要确保不会失败,这是编写异常安详代码的基本。
首先从异常环境下资源打点的问题开始.许多人大概都这么干过:
Type* obj = new Type;
try{ do_something...}
catch(...){ delete obj; throw;}
不要这么做!这么做只会使你的代码看上去杂乱,并且会低落效率,这也是一直以来异常名声不大好的原因之一. 请借助于RAII技能来完成这样的事情:
auto_ptr<Type> obj_ptr(new Type);
do_something...
这样的代码简捷、安详并且无损于效率。当你不体贴或是无法处理惩罚异常时,请不要试图捕捉它。并非利用try…catch才气编写异常安详的代码,大部门异常安详的代码都不需要try…catch。我认可,现实世界并非老是如上述的例子那样简朴,可是这个例子确实可以代表许多异常安详代码的做法。在这个例子中,boost::scoped_ptr是auto_ptr一个更适合的替代品。
此刻来思量这样一个结构函数:
Type() : m_a(new TypeA), m_b(new TypeB){}
假设成员变量m_a和m_b是原始的指针范例,而且和Type内的申明顺序一致。这样的代码是不安详的,它存在资源泄漏问题,结构函数的失败回滚机制无法应对这样的问题。假如new TypeB抛出异常,new TypeA返回的资源是得不到释放时机的.曾经,许多人用这样的要领制止异常:
Type() : m_a(NULL), m_b(NULL){
auto_ptr<TypeA> tmp_a(new TypeA);
auto_ptr<TypeB> tmp_b(new TypeB);
m_a = tmp_a.release();
m_b = tmp_b.release();
}
虽然,这样的要领确实是可以或许实现异常安详的代码的,并且个中实现思想将长短常重要的,在如何实现强担保的异常安详代码中会回收这种思想.然而这种做法不足彻底,至少析构函数照旧要手动完成的。我们仍然可以借助RAII技能,把这件事做得更为彻底:shared_ptr<TypeA> m_a; shared_ptr<TypeB> m_b;这样,我们就可以垂手可得地写出异常安详的代码:
Type() : m_a(new TypeA), m_b(new TypeB){}
假如你以为shared_ptr的机能不能满意要求,可以编写一个接口雷同scoped_ptr的智能指针类,在析构函数中释放资源即可。假如类设计成不行复制的,也可以直接用scoped_ptr。强烈发起不要把auto_ptr作为数据成员利用,scoped_ptr固然名字不大好,可是至少很安详并且不会导致杂乱。
RAII技能并不只仅用于上述例子中,所有必需成对呈现的操纵都可以通过这一技能完成而不必try…catch.下面的代码也是常见的:
a_lock.lock();
try{ ...} catch(...) {a_lock.unlock();throw;}
a_lock.unlock();
#p#分页标题#e#
可以这样办理,先提供一个成对操纵的帮助类:
struct scoped_lock{
explicit scoped_lock(Lock& lock) : m_l(lock){m_l.lock();}
~scoped_lock(){m_l.unlock();}
private:
Lock& m_l;
};
然后,代码只需这样写:
scoped_lock guard(a_lock);
do_something...
清晰而优雅!继承考查这个例子,假设我们并不需要成对操纵, 显然,修改scoped_lock结构函数即可办理问题。然而,往往要领名称和参数也不是那么牢靠的,怎么办?可以借助这样一个帮助类:
template<typename FEnd, typename FBegin>
struct pair_guard{
pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();}
~pair_guard(){m_fe();}
private:
FEnd m_fe;
...//克制复制
};
typedef pair_guard<function<void () > , function<void()> > simple_pair_guard;
好了,借助boost库,我们可以这样来编写代码了:
simple_pair_guard guard(bind(&Lock::unlock, a_lock), bind(&Lock::lock, a_lock) );
do_something...
我认可,这样的代码不如前面的简捷和容易领略,可是它更机动,无论函数名称是什么,都可以拿来结对。我们可以增强对bind的运用,团结占位符和reference_wrapper,就可以处理惩罚函数参数、动态绑定变量。所有我们在catch表里的沟通事情,交给pair_guard去完成即可。
考查前面的几个例子,也许你已经发明白,所谓异常安详的代码,竟然就是如何制止try…catch的代码,这和直觉好像是违背的。有些时候,工作就是如此违背直觉。异常是无处不在的,当你不需要体贴异常可能无法处理惩罚异常的时候,就应该制止捕捉异常。除非你规划捕捉所有异常,不然,请务必把未处理惩罚的异常再次抛出。try…catch的方法当然可以或许写出异常安详的代码,可是那样的代码无论是清晰性和效率都是难以忍受的,而这正是许多人报复C++异常的来由。在C++的世界,就应该凭据C++的法例来行事。
假如凭据上述的原则行事,可以或许实现根基担保了吗?诚实地说,基本设施有了,但能力上还不足,让我们继承阐明不足的部门。
对付一个要领通例的执行进程,我们在要领内部大概需要多次修改工具状态,在要领执行的半途,工具是大概处于犯科状态的(犯科状态 != 未知状态),假如此时产生异常,工具将变得无效。操作前述的手段,在pair_guard的析构中修复工具是可行的,但缺乏效率,代码将变得巨大。最好的步伐是……是制止这么作,这么说有点不老实,但并非毫无原理。当工具处于犯科状态时,意味着此时而今工具不能安详重入、不能共享。现实一点的做法是:
a.每一次修改工具,都确保工具处于正当状态。
b.可能当工具处于犯科状态时,所有操纵决不会失败。
在接下来的强担保的接头中细述如何做到这两点。
强担保是事务性的,这个事务性和数据库的事务性有区别,也有共通性。实现强担保的原则做法是:在大概失败的进程中计较出工具的方针状态,可是不修改工具,在决不失败的进程中,把工具替换到方针状态。考查一个不安详的字符串赋值要领:
string& operator=(const string& rsh){
if (this != &rsh){
myalloc locked_pool(m_data);
locked_pool.deallocate(m_data);
if (rsh.empty())
m_data = NULL;
else{
m_data = locked_pool.allocate(rsh.size() + 1);
never_failed_copy(m_data, rsh.m_data, rsh.size() + 1);
}
}
return *this;
}
locked_pool是为了锁定内存页。为了接头的简朴起见,我们假设只有locked_pool结构函数和allocate是大概抛出异常的,那么这段代码连根基担保也没有做到。若allocate失败,则m_data取值将是犯科的。参考上面的b条目,我们可以这样修改代码:
myalloc locked_pool(m_data);
locked_pool.deallocate(m_data); //进入犯科状态
m_data = NULL; //立即再次回到正当状态,且不会失败
if(!rsh.empty()){
m_data = locked_pool.allocate(rsh.size() + 1);
never_failed_memcopy(m_data, rsh.m_data, rsh.size() + 1);
}
此刻,假如locked_pool失败,工具不产生改变。假如allocate失败,工具是一个空字符串,这既不是初始状态,也不是我们预期的方针状态,但它是一个正当状态。我们阐发了实现根基担保所需要的能力部门,团结前述的基本设施(RAII的运用),完全可以实现根基担保了…哦,其实照旧有一点疏漏,不外,那就留到最后吧。
#p#分页标题#e#
让上面的代码实现强担保:
myalloc locked_pool(m_data);
char* tmp = NULL;
if(!rsh.empty()){
tmp = locked_pool.allocate(rsh.size() + 1);
never_failed_memcopy(tmp, rsh.m_data, rsh.size() + 1); //先生成方针状态
}
swap(tmp, m_data); //工具安详进入方针状态
m_alloc.deallocate(tmp); //释放原有资源
强担保的代码多利用了一个局部变量tmp,先计较出方针状态放在tmp中,然后在安详进入方针状态,这个进程我们并没有损失什么对象(代码清晰性,机能等等)。看上去,实现强担保并不比根基担保坚苦几多,一般而言,也确实如此。不外,别太自信,举一种典范的很难实现强担保的例子,对付区间操纵的强担保:
for (itr = range.begin(); itr != range.end(); ++itr){
itr->do_something();
}
假如某个do_something失败了,range将处于什么状态?这段代码仍然做到了根基担保,但不是强担保的,按照实现强担保的根基原则,我们可以这么做:
tmp = range;
for (itr = tmp.begin(); itr != tmp.end(); ++itr){
itr->do_something();
}
swap(tmp, range);
好像很简朴啊!呵呵,这样的做法并非不行取,只是有时候行不通。因为我们特别支付了机能的价钱,并且,这个价钱大概很大。无论如何,我们叙述了实现强担保的要领,怎么取舍则由您抉择了。 接下来接头最后一种异常安详担保:不会失败。
凡是,我们并不需要这么强的安详担保,可是我们至少必需担保三类进程不会失败:析构函数,释放类函数,swap。析构和释放函数不会失败,这是RAII技能有效的基石,swap不会失败,是为了“在决不失败的进程中,把工具替换到方针状态”。我们前面的所有接头都是成立在这三类进程不会失败的基本上的,在这里,补充了上面的谁人疏漏。
一般而言,语言内部范例的赋值、取地点等运算是不会产生异常的,上述三类进程逻辑上也是不会产生异常的。内部运算中,除法运算大概抛出异常。可是地点会见错凡是是一种错误,而不是异常,我们本应该在前条件查抄中就发明的这一点的。所有不会产生异常操纵的简朴累加,仍然不会导致异常。 好了,此刻我们可以总结一下编写异常安详代码的几条准则了:
1.只在应该利用异常的处所抛出异常
2.假如不知道如那里理惩罚异常,请不要捕捉(截留)异常。
3.充实利用RAII,旁路异常。
4.尽力实现强担保,至少实现根基担保。
5.确保析构函数、释放类函数和swap不会失败。
别的,尚有一些语言细节问题,因为和这个主题有关也一并列出:
1.不要这样抛出异常:throw new exception;这将导致内存泄漏。
2.自界说范例,应该捕捉异常的引用范例:catch(exception& e)或catch(const exception& e)。
3.不要利用异通例范,纵然是空异通例范。编译器并不担保只抛出异通例范答允的异常,更多内容请参考相关书籍。