关于C++中异常的争论何其多也,但往往是一些不合事实的误解。异常曾经是一个难以用好的语言特性,幸运的是,随着C++社区经验的积累,今天我们已经有足够的知识轻松编写异常安全的代码了,而且编写异常安全的代码一般也不会对性能造成影响。
使用异常还是返回错误码?这是个争论不休的话题。大家一定听说过这样的说法:只有在真正异常的时候,才使用异常。那什么是“真正异常的时候”?在回答这个问题以前,让我们先看一看程序设计中的不变式原理。
对象就是属性聚合加方法,如何判定一个对象的属性聚合是不是处于逻辑上正确的状态呢?这可以通过一系列的断言,最后下一个结论说:这个对象的属性聚合逻辑上是正确的或者是有问题的。这些断言就是衡量对象属性聚合对错的不变式。
我们通常在函数调用中,实施不变式的检查。不变式分为三类:前条件,后条件和不变式。前条件是指在函数调用之前,必须满足的逻辑条件,后条件是函数调用后必须满足的逻辑条件,不变式则是整个函数执行中都必须满足的条件。在我们的讨论中,不变式既是前条件又是后条件。前条件是必须满足的,如果不满足,那就是程序逻辑错误,后条件则不一定。现在,我们可以用不变式来严格定义异常状况了:满足前条件,但是无法满足后条件,即为异常状况。当且仅当发生异常状况时,才抛出异常。
关于何时抛出异常的回答中,并不排斥返回值报告错误,而且这两者是正交的。然而,从我们经验上来说,完全可以在这两者中加以选择,这又是为什么呢?事实上,当我们做出这种选择时,必然意味着接口语意的改变,在不改变接口的情况下,其实是无法选择的(试试看,用返回值处理构造函数中的错误)。通过不变式区别出正常和异常状况,还可以更好地提炼接口。
对于异常安全的评定,可分为三个级别:基本保证、强保证和不会失败。
基本保证:确保出现异常时程序(对象)处于未知但有效的状态。所谓有效,即对象的不变式检查全部通过。
强保证:确保操作的事务性,要么成功,程序处于目标状态,要么不发生改变。
不会失败:对于大多数函数来说,这是很难保证的。对于C++程序,至少析构函数、释放函数和swap函数要确保不会失败,这是编写异常安全代码的基础。
首先从异常情况下资源管理的问题开始.很多人可能都这么干过:
Type* obj = new Type;
try{ do_something...}
catch(...){ delete obj; throw;}
不要这么做!这么做只会使你的代码看上去混乱,而且会降低效率,这也是一直以来异常名声不大好的原因之一. 请借助于RAII技术来完成这样的工作:
auto_ptrobj_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_ptrtmp_a(new TypeA);
auto_ptrtmp_b(new TypeB);
m_a = tmp_a.release();
m_b = tmp_b.release();
}
当然,这样的方法确实是能够实现异常安全的代码的,而且其中实现思想将是非常重要的,在如何实现强保证的异常安全代码中会采用这种思想.然而这种做法不够彻底,至少析构函数还是要手动完成的。我们仍然可以借助RAII技术,把这件事做得更为彻底:shared_ptrm_a; shared_ptrm_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();
可以这样解决,先提供一个成对操作的辅助类:
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
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>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++的法则来行事。
如果按照上述的原则行事,能够实现基本保证了吗?诚恳地说,基础设施有了,但技巧上还不够,让我们继续分析不够的部分。
C++异常处理机制核心观点
0.如果使用普通的处理方式:ASSERT,return等已经足够简洁明了,请不要使用异常处理机制.
1.比C的setjump,longjump优秀.
2.可以处理任意类型的异常. 你可以人为地抛出任何类型的对象作为异常.
throw 100;
throw \"hello\";
3.需要一定的开销,频繁执行的关键代码段避免使用 C++异常处理机制.
4.其强大的能力表现在:
A.把可能出现异常的代码和异常处理代码隔离开,结构更清晰.
B.把内层错误的处理直接转移到适当的外层来处理,化简了处理流程.传统的手段是通过一层层返回错误码把错误处理转移到上层,上层再转移到上上层,当层数过多时将需要非常多的判断, 以采取适当的策略.
C.局部出现异常时,在执行处理代码之前,会执行堆栈回退,即为所有局部对象调用析构函数,保证局部对象行为良好.
D.可以在出现异常时保证不产生内存泄漏.通过适当的try,catch布局,可以保证delete pobj;一定被执行.
E.在出现异常时,能够获取异常的信息,指出异常原因.并可以给用户优雅的提示.
F.可以在处理块中尝试错误恢复.保证程序几乎不会崩溃. 通过适当处理,即使出现除0异常,内存访问违例,也能让程序不崩溃,继续运行,这种能力在某些情况下及其重要.
以上ABCDEF可以使你的程序更稳固,健壮,不过有时让程序崩溃似乎更容易找到原因,程序老是不崩溃,如果处理结果有问题,有时很难查找. 5.并不是只适合于处理’灾难性的’事件.普通的错误处理也可以用异常机制来处理,不过如果将此滥用的话,可能造成程序结构混乱,因为异常处理机制本质上是程序处理流程的转移,不恰当的,过度的转移显然将造成混乱.许多人认为应该只在’灾难性的’事件上使用异常处理,以避免异常处理机制本身带来的开销,你可以认为这句话通常是对的.
6.先让程序更脆弱,再让程序更坚强.首先,它使程序非常脆弱,稍有差错,马上执行流程跳转掉,去寻找相应的处理代码,以求适当的解决方式.很像一个人身上带着许多药品,防护工具出行,稍有头晕,马上拿出清凉油;遇到蚊子立刻拿出电蚊拍灭之.
WINDOWS:
7.将结构化异常处理结合/转换到C++异常对象,可以更好地处理WINDOWS程序出现的异常.
8.尽一切可能使用try,catch,而不是win32本身的结构化异常处理或者MFC中的TRY,CATCH宏.
用得恰到好处,方显C++异常之美妙!catch(…)
{
}
这三个点并不是说要省略什么.相反,你需要在程序中实际输入这三个点.这是一个很好的默认CATCH块,应该把它放在其他所有CATCH块之后.
1.7异常规范:
Double safe_divide(int top,int bottom) throw(DivideByZero);
假如在函数中抛出一个异常,但异常规范中并未列出这个异常(也没有在函数内部捕捉),会发生什么事情?在这种情况下,程序会终止.尤其要注意的是,假如一个异常在函数中招聘但既没有在异常规范中列出,也没有在函数内部捕捉,那么它不会被任何catch块捕捉,而是直接导致程序终止.记住,如果完全没有异常规范列表,就连空白的都没有,那么效果等同于在规范列表中列出所有异常.在这种情况下,抛出一个异常不会终止程序.
注意,异常规范是为那些准备跑到函数外部的异常而准备的.如果它们不跑到函数外部,就不归入异常规范.如果它们要跑到函数外部,就应该归入异常规范,无论它们起源于何处.如果在函数定义内部的一个try块中抛出一个异常,而且在函数定义内部的一个catch块中捕捉这个异常这个异常的类型就不需要在异常规范中列出.如果函数定义包括对另一个函数的调用,而另一个函数可能招聘一个它自已不会被捕捉的异常就应该在异常规范中列出异常的类型.
要表示一个函数不应抛出任何不在函数内部捕捉的异常,需要使用一个空白异常规范.
Void some_function() throw();
几种方式可以总结如下:
Void some_funtion() throw(DivideByZero,OtherExecption);
//DivideByZero或OtherException类型的异常会被正常处理.
//至于其他任何异常,如果抛出后未在函数主体中捕捉,就会终止程序.
Void some_function()throw();
//空异常列表:一旦抛出任何未在函数主体中捕捉的异常就会终止程序.
Void some_function();
//正常处理所有类型的所有异常.
1.8陷阱:派生类中的异常规范
在派生类中重定义或覆盖一个函数定义时,它应具有与基类中一亲友的异常规范,或至少应该在新的异常规范中给出基类异常规范的一个子集.换言之,重定义或覆盖一个函数定义时,不可在异常规范中添加新异常.但是,如果愿意,可删减基类中原有的异常.之所以有这个要求,是因为在能够使用基类对象的任何地方,都能使用一个派生类对象.因此,重定义或覆盖的函数必须兼容于为基类对象编写的任何代码.
下面的内容是我从<<C++面向对象程序设计(第五版)>>找到值得学习的内容assert语句我们曾用以下语句测试一个名为in_stream的文件是否成功打开:
If(in_stream.fail())
{
Cout<<”input file opening failed.\\n”;
Exit(1);
}
可用assert(断言)语句编写同样的测试,如下所示:assert(!in_stream.fail());
注意,这种情况下,我们要插入一个求反操作符(!),才能获得同样的效果,因为我们要断言的是文件打开操作”没有失败”.
Assert语句由标识符assert,一个逻辑表达式(包含在一对圆括号内)各一个分号构成.可以使用任何逻辑表达式.如果逻辑表达式false,程序就终止运行,并给出一个错误消息.如果逻辑表达式示值为true,就什么情况都不会发生,程序继续执行assert语句之后的下一条语句.因此,assert 语句是在程序中进行错误检查的一种精简方式.
Assert语句在cassert库中定义,所以使用assert语句的任何程序都必须包含以下
include预编译指令:
#include<cassert>
Assert是一个宏(类似于函数的一种结构),所以有必要在一个库中定义它.
使用assert语句的一个好处是可以将其关闭.你可在自己的程序中用assert语句来高度程序,再将其关闭使用户看不到他们无法理解的错误消息.关闭assert语句,还能减少程序执行这些语句的开销.要关闭程序中的所有assert语句,请在include预编译指令之前添加#define NDEBUG,如下所示:
#define NDEBUG
#include<cassert>
因此,如果在进行了全面高度的程序中插入#define NDEBUG,就会关闭程序中的所有assert语句.如果以后改动了程序,可删除程序中的#define NDEBUG重新打开assert语句第6点补充为:
6.先让程序更脆弱,再让程序更坚强.首先,它使程序非常脆弱,稍有差错,马上执行流程跳转掉,去寻找相应的处理代码,以求适当的解决方式,如果找不到任何解决办法(异常没有被捕获并处理),立刻(结束程序运行)。
很像一个人身上带着许多药品,防护工具出行,稍有头晕,马上拿出清凉油;遇到蚊子立刻拿出电蚊拍灭之,遇到哪怕是蚊虫叮咬,如果找不到电蚊拍,他马上自杀!(这个家伙真是脆弱:) 以致于我们必须为他准备所有的防护工具一旦我们为他准备了全部适当的防护工具,他就变成一个非常坚强的人了!)。
当然,代价是这个家伙变得比较笨重,行动迟缓。
近似的公理:
1 异常是同步的。即只能发生在函数边界。预定义类型的算术操作、预定义类型的不会导致异常
2 对象的销毁是异常安全的。即析构函数、operator delete 和operator delete[] 是异常安全的。
3 swap函数不会导致异常