分享

c++中的异常处理

 贫穷的小悍马 2010-10-15
c++中的异常处理
2007-06-14 18:13

简介

大型应用软件往往是分层构建的。在最底层你会发现库函数,API函数,和私有的底层函数。然而在最高层则是用户接口组件,比如一个电子制表软件让用户填写数据表单。下面来看一种普通的航空订票系统:它的最高端是由一些GUI组件所组成,用来在用户的屏幕上显示内容。这些高端组件与那些封装了数据库API的数据存取对象相互作用。再往底层一些,那些数据库API与数据库引擎相交互,然而数据库引擎自己又会调用系统服务来处理底层的硬件资源,比如物理内存,文件系统和安全模型。一般情况下,及其严格的运行期错误会在这些底层代码中被检测出来,但是它们不能-----或者说不应该----试图自己处理这些错误。解决这些严格的运行期错误的责任应该由高端组件来承担。为了解决一个错误,高端组件必须得到错误发生的通知。本质上,错误处理包括错误检测和通知高端组件。这些组件依次处理错误并且试图从错误中恢复。

传统的错误处理方法

在早些时期,C++本身并没有处理运行期错误的能力。取而代之的是那些传统的C方法。这些方法可以被归为三类设计策略:
返回一个状态码来表明成功或失败
把错误码赋值给一个全局标记并且让其他的函数来检测
终止整个程序

上述的任何一个方法在面向对象环境下都有明显的缺点和限制。其中的一些根本就不可接受,尤其是在大型应用程序中。接下来的部分将会仔细检查一下这些方法,目的是发现他们与生俱来的限制和危险性。

返回一个错误码

在某种程度上这个方法是有用的,比如一个小型程序有着一致而且有限的错误码存在,并且严格的报告错误和检查一个函数返回值的策略被应用。然而,这种方法也有着显著的局限性;例如,错误类型和它们的列举值必须标准化。因为一个库的实现者可能选择返回值0来代表一个错误,然而另一个实现者却选择0来代表成功并且用那些非0值代表出现错误。通常,那些返回码会在一个公共头文件中以符号常量的形式存在,从而在整个软件的开发过程中或者在一个开发团队里达成一致。但是,这些码并不是标准的。

不用说,在结合那些不兼容的软件库的时候,如何处理非标准的错误码将会是一件极其头疼的事。另外一个缺点是对于每一个返回码都必须查阅和解释------一个乏味并且昂贵的操作。这个策略的实现需要调用者在每一次调用的时候对返回值进行检查,如果没有这样做将会导致运行期错误。当一个错误码被检测,就会终止正常的执行流程并且把错误码传递给调用者。那些附加的包裹每一个函数调用的代码会很轻易的使程序的大小翻倍并且引起软件维护和程序可读性的降低。更糟的是,有时要想返回一个error value是不可能的。例如,构造函数没有返回值,所以就不能应用这种方法在对象构造失败的情况下报告错误。

求助于全局标记

一个可以选择的用来报告运行期错误的途径是使用全局标记,它表明了最后的操作是否成功。不像返回码策略,这个方法是标准化的。C 的<errno.h>头文件中定义了一种机制用来检查和给一个全局整型标记errno赋值。这种策略固有的缺陷也是不能被忽视的。在一个多线程环境中,被一个线程赋予了一个错误码的errno有可能不经意的被另一个线程所改写,而调用者还未对errno进行检查。另外,对错误码而不是一个更为可读的信息的使用是很不利的,因为那些错误码可能会在不同的环境中不兼容。最终,这种方法需要严格的良好的编程样式,也就是不断的对errno的当前值进行检查。

全局标记策略和函数返回值策略是相似的:二者都提供一种机制来报告错误,但是二者却都不能保证错误被处理。例如,一个函数没有成功打开一个文件可以通过给errno赋予一个合适的值来表明错误的发生。然而,它不能阻止另一个函数试图写入和关闭那个文件。更进一步,如果errno表明一个错误并且程序员检测到而且按照预期处理了它,那么errno还应该被显式的复位。如果一个程序员忘记了做这件事,那么将会引起其他函数误以为错误还没有被处理,从而去校正那个问题,引起不可预知的结果。

终止程序

最为残酷的处理运行期错误的方法是简单的终止程序。这种解决方案去除了上面两种方法的一些缺点;例如,没有必要反复的检查每个函数返回值的状态,而且程序员也不必赋值给一个全局标记,反复的测试和清除它的值。在标准C的函数库中有两个函数用来终止一个程序:exit()和abort()。exit()被调用能够表明程序被成功终止,或者它可以在遇到运行期错误的时候被调用。在把控制权交还给运行环境之前,exit()首先会清空流和关闭打开的文件。abort()却不一样,它表示程序被意外终止,不会清空流和关闭打开的文件。

关键性的程序不应该在任何运行期错误存在的情况下突然终止。如果一个生命支持系统突然停止工作仅仅是因为它的控制器检测到0做除数,那么将是一种灾难。同样,一个控制由人驾驶的航天飞机自动运行的计算机系统也不应该因为暂时的和地面控制系统失去联系就停止工作。类似的,电话公司的账目系统或者银行系统都不应该在运行期错误出现的时候就中止。健壮的真实世界的应用程序应该做的更好。
程序终止甚至对于应用程序都是有问题的。一个检测到错误的函数通常都没有必要的信息来衡量错误的严重性。例如一个内存分配函数并不能说出内存分配失败是由于用户正在使用调试器,网页浏览器,电子制表软件,文字处理软件,还是由于系统因为硬件错误变得不稳定。在第一种情况下,系统可以简单的显示一条信息来告诉用户关闭不必要的应用程序。第二种情况下,就需要一种更为残酷的措施了。然而,在终止程序的策略下,那个内存分配函数就会简单的终止程序,而不考虑错误的严重性。这种方法在一些关键性应用程序中是无法应用的。好的系统设计应该保证运行期错误被检测和报告,但是它也应该确保最小限度的容错水平。

终止程序在极限环境下或者在调试阶段是可以被接受的。然而,abort()和exit()却不应该在面向对象环境中使用,甚至即使在调试阶段,因为他们并没有意识到C++对象模型的存在。

exit()和abort()不销毁对象

对象可以持有从构造函数或者某个成员函数中获得的资源:从free store中分配的内存,文件句柄,通信端口,I/O设备等等。这些资源必须在适当时候被释放。通常,资源都是由析构函数来释放。这种设计方法被称为resource initialization is acquisition。在栈上建立的局部对象会自动销毁。然而abort() 和exit()并不调用这些局部对象的析构函数。因此,程序的意外终止将会引起无法挽回的损害:数据库被破坏,文件可能丢失,并且一些有价值的数据可能丢失。基于这个原因,请不要在面向对象环境中使用abort()和exit()。

进入异常处理

正如你所见,传统C的错误处理方法并不适合C++,C++的一个设计目标就是让用C++进行大规模软件开发比C更好更安全。

C++的设计者们已经意识到缺乏合适的错误处理机制使得实现这一目标相当的困难。他们试图寻找一种完全摆脱C的错误处理缺陷的解决方案。其中的一种想法就是建立在当异常被触发的时候程序自动把控制权传递给系统。机制必须简单,并且它能够使程序员从不断的检查一个全局标记或者返回值的苦差事中解脱出来。另外,它还必须保证异常处理程序能够自动获得异常信息。最终它还要确保当一个异常没有在本地处理的时候,本地对象能够被适当的销毁,并且把它所持有的资源释放。

1989年,在多年的研究和多方建议下,异常处理进入C++。C++并不是第一个对结构化运行期错误处理进行支持的语言。早在20世纪60年代,PL/1就提供了一种内建的异常处理机制;Ada也在20世纪80年代提供了自己的异常处理,另外还有几种语言也做到了这一点。但是这些异常处理模型没有一个适合C++对象模型和程序结构。因此,被提议的C++异常处理是独一无二的,并且它已经作为了一种模型出现在一些新产生的语言之中。

异常处理机制的实现被证明是一种挑战。第一个C++编译器,cfront,在UNIX环境下运行。和许多UNIX编译器一样,它首先是作为一个翻译器把C++代码转换成C,接着再编译C代码。Cfront 4.0计划引入异常处理,然而,异常处理机制的实现是如此的复杂,以至于cfront 4.0的开发团队在用了一年时间设计它之后完全的放弃了这个项目。Cfront 4.0再也没有出台。然而,异常处理却成为了标准C++的有机组成部分。后来出现的一些编译器都支持了它。在接下来的部分里将会解释为什么在cfront以及任何编译器下实现异常处理是如此的困难。

实现异常处理所面临的挑战

实现异常处理所遇到的困难主要来自于以下几个因素:

第一,实现必须保证对于某一异常的合适的handler被找到。

第二,异常对象必须是多态的;这样,当实现无法通过派生类对象定位handler的时候可以考虑基类的handler。这种需要表明必须引入运行期类型检测。然而那时C++还没有任何运行期类型检测的能力。因此这种能力必须首先被实现。

作为一个附加的复杂性,实现必须能够调用所有局部对象的析构函数。这个过程被称为stack unwinding 。因为早期的C++编译器首先要把C++源文件转换为纯C,然后再把C代码编译成机器码。异常处理的实现者们不得不用C来实现运行期类型鉴别和stack unwinding。幸运的是,这些障碍已经被克服。

应用异常处理

异常处理是一种灵活并且精巧的工具。它克服了C的传统错误处理方法的缺点并且能够被用来解决一系列运行期错误。但是,异常处理也像其他语言特性一样,很容易被误用。为了能够有效的使用这一特性,理解运行期机制是如何工作的以及相关的性能花费是非常重要的。接下来的部分里将会进入异常处理的内部并且论证如何使用这一工具来建立安全的应用系统。

异常处理要素

异常处理是一种把控制权从异常发生的地点转移到一个匹配的handler的机制。异常是内建数据类型变量或者是对象。异常处理机制包括四个部分:a try block,一个或多个和try block相关的handler,throw表达式,以及异常自己。Try block包含可能抛出异常的代码。例如:

try
            {
                 int * p = new int[1000000]; //may throw std::bad_alloc
            }

一个try block后面将跟有一个或多个catch语句或者说是handlers, 每一个handler 处理不同类型的异常。例如:

try
            {
                 int * p = new int[1000000]; //may throw std::bad_alloc
                 //...
            }
            catch(std::bad_alloc& )
            {
            }
            catch (std::bad_cast&)
            {
            }

handler仅仅被在try block中的throw表达式以及函数所调用。throw表达式包括一个关键字throw以及assignment expression。例如:

try
            {
                 throw 5; // 5 is assigned to n in the following catch statement
            }
            catch(int n)
            {
            }

throw表达式和返回语句很相似。empty throw是没有操作数的throw语句。例如:

throw;

在handler中的empty throw表明它在重新抛出异常,后面我们会讨论到它。另外,如果目前没有异常被处理,那么执行一个empty throw将会调用terminate()。

Stack Unwinding

当一个异常被抛出,运行时机制首先在当前的作用域寻找合适的handler。如果不存在这样一个handler,那么将会离开当前的作用域,进入更外围的一层继续寻找。这个过程不断的进行下去直到合适的handler被找到为止。此时堆栈已经被解开,并且所有的局部对象被销毁。如果始终都没有找到合适的handler,那么程序将会终止。注意,C++保证局部对象被适当的销毁仅仅是在抛出的异常被处理的情况下。一个未被扑获得异常是否引起局部对象的销毁由实现决定的。为了保证局部对象的析构函数在异常未被捕获情况下也能够被正常调用,你应该在main()里加入捕获任何异常的catch语句。例如:

int main()
            {
                 try
                 {
                     //...
                 }
                 catch(std::exception& stdexc) // handle expected exceptions
                 {
                     //...
                 }
                 catch(...) // ensure proper cleanup in the case of an uncaught exception
                 {
                 }
                
                 return 0;
            }

stack unwinding的过程就好比一个返回语句序列,每一个都返回相同的对象给它的调用者。

传递异常对象给handler

一个异常能够按值或者按引用的方式传递给它的handler。为异常对象分配的内存是通过一种未被定义的途径(但是并没有在自由存储区)。一些实现使用专门的异常堆栈,在那里,异常对象被创建。当一个异常按引用的方式传递,handler获得是在异常堆栈上建立的对象的引用。通过引用方式传递异常保证了它的多态行为。按值传递的异常被建立在调用者的堆栈上。例如:

#include <cstdio>
            class ExBase {/*...*/};
            class FileEx: public ExBase {/*...*/};
            void Write(FILE *pf)
            {
                 if (pf == NULL) throw FileEx();
                 //... process pf normally
            }
            int main ()
            {
                 try
                 {
                     Write(NULL); //will cause a FileEx exception to be thrown
                 }
                 catch(ExBase& exception) //catch ExBase or any object derived from it
                 {
                     //diagnostics and remedies }
                 }
            }

按值传递异常将会造成反复的复制对象,并且它的花费是昂贵的,因为异常对象在匹配的handler被找到以前会被构造和销毁许多次。然而,在比较罕见的情况下也会发生按值传递,由于为了保持应用系统的整体性,性能考虑往往被放在了第二位。

异常类型匹配

异常的类型决定了哪个handler能够捕获它。异常的匹配规则比函数重载的匹配规则更为严格。考虑下面这种情况:

try
            {
                 throw int();
            }
            catch (unsigned int) //will not catch the exception from the previous try-block
            {
            }

抛出异常的类型是int型,然而handler却期待一个unsigned int。异常处理机制不认为二者是能够匹配的类型;结果,抛出的异常没有被捕获。异常匹配规则仅仅允许一个非常有限的转换集。对于一个异常E和一个带有T或T&参数的handler,符合下面的条件可以进行匹配:

T和E是同一类型(const 和volatile被忽略)
T是E的没有歧义的公共基类

如果E和T都是指针类型,当二者的类型相同时可以进行匹配或者E所指向对象的类型公有无歧义的继承自T指向对象的类型。

作为对象的异常

正如你所发现的,传统的通过返回一个整型错误码的方法在OOP中已经不再适用。C++异常处理机制提供了更多的弹性,安全性和稳固性。一个异常既可以是int 或char等基本类型,也可以是更为丰满的对象,有着数据成员和成员函数。这样一个对象可以为handler提供更多的选择进行恢复。一个聪明的异常对象,可以通过成员函数返回错误的详细描述,而不是让handler查阅某个表或文件。它也可以拥有在错误被适当处理之后使程序从运行期错误中恢复的成员函数。考虑有这样一个日志类想要添加新的纪录到一个已存在的日志文件中:如果打开日志文件失败,它会抛出一个异常。当它被匹配的handler所捕获,异常对象能够拥有一个成员函数,这个成员函数建立一个对话框。操作者可以从对话框中选择恢复方法,包括建立一个新的日志文件,选择另一个日志文件,或者是允许系统在没有日志的情形下运行。

Exception Specification

一个函数可以通过指定一个它所能抛出的异常列表来提醒它的用户。Exception specifications在用户只能看到函数的原型但是却无法获得它的源文件的时候将会十分的有用。下面是一个指定异常的例子:

class Zerodivide{/*..*/};
            int divide (int, int) throw(Zerodivide); // function may throw an exception
            // of type Zerodivide, but no other

如果你的函数永远不会抛出任何异常,它可以像下面这样声明:

bool equals (int, int) throw(); //no exception is thrown from this function

注意一个函数被声明为没有exception specification 例如:

bool equals (int, int);

Exception specification在运行期生效

一个exception specification不会在编译期被检查,而是在运行期。当一个函数试图抛出一个在exception specification中未被指定的异常的时候,异常处理机制将会检测到这种违规并且调用标准函数unexpected()。unexpected()的默认行为是调用terminate()终止程序。违背exception specification就好比是一个bug,不应该发生,这就是为什么默认行为是终止程序。不过默认的行为也可以被改变,通过使用函数set_unexpected()。

因为exception specifications在运行期才有效,所以编译期可能会故意忽略那些违背exception specifications的代码。好比下面:

int f(); // no exception specification, f can throw any type of exception
            void g(int j) throw() // g promises not to throw any exception at all
            {
                 int result = f(); // if f throws an exception, g will violate its guarantee
                 //not to throw an exception. still, this code is legal
            }

在上面这个例子中,函数g()并不允许抛出任何异常。它调用函数f(),然而f()却可能抛出任何异常因为它没有exception specification。如果f()抛出一个异常,它将会通过g()传播出去,但是这却破坏了g()不会抛出任何异常的保证。这也许看起来会很奇怪,有一些违背在编译期就应该被发现报错的,为什么一定要等到运行期呢?然而许多问题并不像想象的那么简单,以下几个原因就要求必须采用运行期检测策略。在前面的那个程序中,f()可能是一个被遗留下来的C函数。我们不可能强迫每个C函数有exception specification。并且因为这个原因就强迫程序员在g()中写不必要的try和catch(…)块也是不实际的。通过强迫exception specification只在运行期才有效,C++采取了“信任程序员”的策略而不是强加负担给程序员和实现。

Exception specification的一致性

C++需要派生类中的exception specification与基类保持一致。这意味着派生类的virtual function重载函数的exception specification必须是基类的限制性子集,例如:

// various exception classes
            class BaseEx{};
            class DerivedEx: public BaseEx{};
            class OtherEx {};
            class A
            {
            public:
            virtual void f() throw (BaseEx);
            virtual void g() throw (BaseEx);
            virtual void h() throw (DerivedEx);
            virtual void i() throw (DerivedEx);
            virtual void j() throw(BaseEx);
            };
            class D: public A
            {
            public:
            void f() throw (DerivedEx); //OK, DerivedEx is derived from BaseEx
            void g() throw (OtherEx); //error; exception specification is
            //incompatible with A's
            void h() throw (DerivedEx); //OK, identical to the exception
            //specification in base
            void i() throw (BaseEx); //error, BaseEx is not a DerivedEx nor is it
            //derived from DerivedEx
            void j() throw (BaseEx,OtherEx); //error, less restrictive than the
            //specification of A::j
            };

相同的一致性限制也应用于函数指针。一个拥有exception specification函数指针只能被赋予一个有着相同或更为局限的exception specification的函数。这说明一个没有exception specification的函数指针不能被赋予一个有exception specification的函数。注意,因为exception specification不能被认为是函数类型的一部分,因此你不能声明两个仅仅是exception specification不同的函数。例如:

void f(int) throw (Y);
            void f(int) throw (Z); //error; redefinition of 'void f(int)'

同样的原因,声明一个包含exception specification的typedef也是错误的:

typedef void (*PF) (int) throw(Exception); // error

在对象构造和销毁时出现异常

构造函数和析构函数被自动调用,并且它们不能够利用返回值来表明发生运行期错误。从表面上看,在对象构造和销毁时抛出一个异常似乎是报告运行期错误的最好方法。但事实上你还必须考虑一些额外的因素。你尤其应该对从析构函数中抛出异常保持警惕。

从析构函数中抛出异常是危险的

从析构函数中抛出异常是不应该被推荐的,这是因为一个析构函数可能会在另一个异常进行stack unwinding的时候被调用,在这种情况下,异常处理机制就会调用terminate()终止程序。如果你真的想从一个析构函数中抛出异常的话,一种可取的做法是首先检查一下是否还有未被捕获的异常存在。

检查未被捕获的异常

一个异常被捕获是在它相应的handler被找到的情况下。为了检查一个异常是否被捕获,你可以使用标准函数uncaught_exception()(它被定义在标准头文件)。例如:

class FileException{};
            File::~File() throw (FileException)
            {
            if ( close(file_handle) != success) // failed to close current file?
            {
            if (uncaught_exception() == true ) // is there any uncaught exception
            //being processed currently?
            return; // if so, do not throw an exception
            throw FileException(); // otherwise, it is safe to throw an exception
            // to signal an error
            }
            return; // success
            }

然而,一个更好的选择是直接在析构函数内部处理异常,而不是让他们扩散到外面。例如:

void cleanup() throw (int);
            class C
            {
            public:
            ~C();
            };
            C::~C()
            {
            try
            {
            cleanup();
            }
            catch(int)
            {
            //handle the exception within the destructor
            }
            }

如果一个异常被函数cleanup()抛出,那么它在析构函数内部就被处理。否则,被抛出的异常就会传播到析构函数的外部,并且如果这个析构函数是在stack unwinding 的过程中被调用,那么程序将会通过terminate()的调用而终止。

全局对象:构造和销毁

我们都知道,全局对象的构造发生在程序开始之前。因此,任何从全局对象的构造函数中抛出的异常将不会被捕获。这一点对于全局对象的析构函数也是一样的-----全局对象的析构函数在程序结束之后被运行。因此,一个从全局对象的析构函数中抛出的异常也不会被捕获。

高级异常处理技术

简单的try-throw-catch模型可以被扩展来处理更为复杂的运行期错误。这一节将会讨论一些更为高级的异常处理技术,包括异常层次,重新抛出异常,function try blocks以及auto_ptr 类。

标准异常

C++定义了一个标准异常层次,当在运行时发生反常情形时抛出。标准异常类从std::exception(在头文件中定义)派生。这一层次使得应用程序能够在单一的catch语句中捕获这些异常:

catch (std::exception& exc)
            {
            // handle exception of type std::exception as well as
            //any exception derived from it
            }

那些通过语言内建操作符抛出的标准异常是:

std::bad_alloc //by operator new
            std::bad_cast //by operator dynamic_cast < >
            std::bad_typeid //by operator typeid
            std::bad_exception //thrown when an exception specification of 

所有的标准异常都提供了成员函数what(),它返回一个用来描述异常细节的字符串。注意,标准库还有另外一个被它的组件抛出的的异常集合。

异常处理层次

异常在一个自下向上的层次中捕获:派生层次越深的异常越先被处理,例如:

#include <stdexcept>
            #include <iostream>
            using namespace std;
            int main()
            {
            try
            {
            char * buff = new char[100000000];
            //...use buff
            }
            catch(bad_alloc& alloc_failure) // bad_alloc is
            //derived from exception
            {
            cout<<"memory allocation failure";
            //... handle exception thrown by operator new
            }
            catch(exception& std_ex)
            {
            cout<< std_ex.what() <<endl;
            }
            catch(...) // exceptions that are not handled elsewhere are caught here
            {
            cout<<"unrecognized exception"<<endl;
            }
            return 0;
            }

派生层次越深的handler必须出现在其基类的前面。这是因为handler的匹配过程是按照出现的顺序进行的。因此有可能某个handler永远不会被执行,例如,把一个处理派生类异常的handler放在处理基类异常的handler的后面。例如:

catch(std::exception& std_ex) //bad_alloc exception is always handled here
            {
            //...handle the exception
            }
            catch(std::bad_alloc& alloc_failure) //unreachable
            {
            cout<<"memory allocation failure";
            }

重新抛出异常

异常的抛出表明了一种反常的状态。先捕获到异常的handler试图解决这个问题,但是它如果没有成功或者只完成了部分恢复,那么它可以重新抛出这个异常,让更高一层的try block来处理它。基于这种目的,try blocks可以在一个分等级的顺序上进行嵌套,使得一个从低层重新抛出的异常能够被重新捕获。重新抛出用一个没有操作数的throw语句来表示。例如:

#include <iostream>
            #include <string>
            using namespace std;
            enum {SUCCESS, FAILURE};
            class File
            {
            public: File (const char *) {}
            public: bool IsValid() const {return false; }
            public: int OpenNew() const {return FAILURE; }
            };
            class Exception {/*..*/}; //general base class for exceptions
            class FileException: public Exception
            {
            public: FileException(const char *p) : s(p) {}
            public: const char * Error() const { return s.c_str(); }
            private: string s;
            };
            void func(File& );
            int main()
            {
            try //outer try
            {
            File f ("db.dat");
            func; // 1
            }
            catch(...) // 7
            //this handler will catch the re-thrown exception;
            //note: the same exception type is required
            {
            cout<<"re-thrown exception caught";
            }
            return 0;
            }
            void func(File & f)
            {
            try //inner try
            {
            if (f.IsValid() == false )
            throw FileException("db.dat"); // 2
            }
            catch(FileException &fe) // 3
            //first chance to cope with the exception
            {
            cout<<"invalid file specification" <<fe.Error()<<endl;
            if (f.OpenNew() != SUCCESS) (5)
            //re-throw the original exception and let a higher handler deal with it
            throw; // 6
            }
            }

在上面的例子中,函数func()在main()中的try block里被调用(1)。第二个在func()中的try block抛出一个FileException类型的异常(2)。这个异常被func()内的catch block所捕获(3)。那个catch block试图通过打开一个新文件进行补救,但是失败了(5),并且FileException异常被重新抛出(6)。最终,那个重新抛出的异常被main()中的catch(…)所捕获(7)。

Function try Blocks

Function try blocks是一个函数体本身就含有一个try block以及它的相关handler的函数。比如:

class Bad{};
            void foo()try
            {
            throw Bad();
            }
            catch(...)
            {
            std::cout<<"error catch!!";
            }

function try block使得一个handler能够捕获构造函数中以及初始化列表中发生的异常。然而,它并不像普通异常的handler,function try block很少能够捕获异常继续对象的构建。这是因为被部分构造的对象要被销毁。另外,一个function try block的handler不能执行返回语句(或者说,handler必须通过一个throw离开)。那么究竟function try block的用处是什么呢?handler使得你可以抛出另一个异常而不是你刚才捕获的那个,这样可以阻止一个违背exception specification的情况发生。例如:

class X{};
            C::C(const std::string& s) throw (X) // allowed to throw X only
            try
            : str(s) // str's constructor might throw a bad_alloc exception,
            // might violate C's exception specification
            {
            // constructor function body
            }
            catch (...) //handle any exception thrown from ctor initializer or ctor body
            {
            //...
            throw X(); //replace bad_alloc exception with an exception of type X
            }

在这个例子中,一个string对象首先被创建作为class c 的一个成员。String在它的创建过程中可能抛出一个bad_alloc异常。那个function try block能够捕获bad_alloc异常并且抛出类型为x的异常使得它满足c的构造函数的exception specification的需要。

异常处理的性能分析

异常处理机制主要取决于运行期类型检查,当一个异常被抛出,实现必须确定异常是不是从try block中抛出。如果异常是从try block中抛出的话,实现就需要比较异常的类型,试图从当前的作用域中找到匹配的handler。如果找到控制权将会传递给handler。然而这些都是乐观的情况。如果实现没有找到一个匹配的handler的话,或者异常不是从try block中抛出,那么就会进行stack unwinding,这个过程会一直进行下去直到一个匹配的handler被找到为止。当匹配的handler未被找到,terminate()就会被调用,终止程序。

额外的运行期类型信息

异常处理机制为了完成异常和它的handler之间的运行期匹配,它必须储存关于每个异常对象的类型以及每个catch语句的额外信息。因为异常可以是任何类型,并且也可以是多态的,所以它的动态类型信息必须被获得通过使用runtime type information(RTTI),RTTI给程序在速度和大小上增加了额外的负担。并且只有RTTI还不够,实现也需要运行期代码信息,关于每个函数的结构。这些信息需要拿来确定一个异常是不是从try block中抛出。这个信息可以通过编译器以下面的方式产生:编译器把每个函数体划分为三个部分:第一个部分在try block之外并且没有活动的对象,第二个部分也在try block之外但是有活动的对象而且要通过stack unwinding来进行销毁,第三部分在try block中。

抓住对异常处理的支持

异常处理技术的实现在不同的编译器以及平台下是不一样的。但是它们都会强加额外的负担给程序,即使没有异常被抛出。一些编译器可以选择是否对异常处理进行支持。当异常处理被关掉的时候,那些额外的数据结构,查找表,以及一些附加的代码都不会被生成。然而,关闭异常处理往往很少被选择,因为即使你不直接使用异常,你也会隐含的使用它们:例如operator new,会抛出std::bad_alloc异常;STL容器可能会抛出他们自己的异常;第三方代码库也可能使用异常。因此,只有在你导入纯C代码的时候,才应该考虑关闭异常处理来避免额外的负担。

异常的误用

异常处理并不是用来限制出现错误,一些程序员可能会简单的使用它来作为循环的选择控制结构。例如,一个简单的应用程序让用户输入数据直到一个特定的条件满足:

#include <iostream>
            using namespace std;
            class Exit{}; //used as exception object
            int main()
            {
            int num;
            cout << "enter a number; 99 to exit" << endl;
            try
            {
            while (true) //infinitely
            {
            cin >> num;
            if (num == 99)
            throw Exit(); //exit the loop
            cout<< "you entered: " << num << "enter another number " <<endl;
            }
            }
            catch (Exit& )
            {
            cout<< "game over" <<endl;
            }
            return 0;
            }

在上面的例子中,程序员把一个无限循环放在了try block中,throw语句终止循环并且把控制权传递给后面的catch语句。这种编程样式不应该被推荐。它的效率会非常低下因为异常处理存在。在上面小的演示程序中,或许仅仅是程序样式的差异,但是在大规模的应用系统中,使用异常处理来作为控制选择控制结构的话,那么将会带来显著的效率损失。

结论

C++的异常处理机制克服了传统方法所带来的问题。它使得程序员从检查函数的返回状态的乏味的代码中解放出来。另外一个重要的优点是自动的stack unwinding,它保证了局部活动对象被正确销毁以及他们的资源被安全释放。

实现异常处理机制并不是一项简单的工作。对获取异常的动态类型的需要使得RTTI被引入C++。异常可以按照种类进行分组,标准异常就是一个很好的例子。在最近几年里,一些异常处理机制已经得到了修正。第一个就是把exception specifications加入到了函数原型中。另一个是function try block的引入,它使得程序能够处理从构造函数体或初始化列表中抛出的异常。

异常处理是一个用来有效的处理运行期错误的非常强大并且灵活的工具。然而使用它也是有代价的。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多