异常处理(四) Danny Kalev 翻译: cppbug cpp_bug@hotmail.com
在对象构造和销毁时出现异常
构造函数和析构函数被自动调用,并且它们不能够利用返回值来表明发生运行期错误。从表面上看,在对象构造和销毁时抛出一个异常似乎是报告运行期错误的最好方法。但事实上你还必须考虑一些额外的因素。你尤其应该对从析构函数中抛出异常保持警惕。
从析构函数中抛出异常是危险的
从析构函数中抛出异常是不应该被推荐的,这是因为一个析构函数可能会在另一个异常进行stack unwinding的时候被调用,在这种情况下,异常处理机制就会调用terminate()终止程序。如果你真的想从一个析构函数中抛出异常的话,一种可取的做法是首先检查一下是否还有未被捕获的异常存在。
检查未被捕获的异常
一个异常被捕获是在它相应的handler被找到的情况下。为了检查一个异常是否被捕获,你可以使用标准函数uncaught_exception()(它被定义在标准头文件<stdexcept>)。例如:
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(在<stdexcept>头文件中定义)派生。这一层次使得应用程序能够在单一的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的需要。
(未完待续)