星期日, 三月 11, 2007

C与C++中的异常处理(zt)

1. unexpected()的实现上固有的限制

上次,我介绍了C++标准运行库函数unexpected(),并展示了Visual C++的实现版本中的限制。这次,我想展示所有unexpected()的实现上固有的限制,以及绕开它们的办法。

1.1 异常处理函数是全局的、通用的

我在上次简要地提过这点,再推广一点:过滤unexpected异常的异常处理函数unexpected()是全局的,对每个程序是唯一的。

所有unexpected异常都被同样的一个unexpected()异常处理函数处理。标准运行库提供默认的处理函数来处理所有unexpected异常。你可以用自己的版本覆盖它,这时,运行库会调用你提供的处理函数来处理所有的unexpected异常。

和普通的异常处理函数,如:

catch (int)

{

}

不同,unexpected异常处理函数不“捕获”异常。一旦被进入,它就知道有unexpected异常被抛出,但不知道类型和起因,甚至没法得到运行库的帮助:运行库中没有程序或对象保存这些讨厌的异常。

在最好的情况下,unexpected异常处理函数可以把控制权交给程序的其它部分,也许它们有更好的办法。例如:

#include

using namespace std;

void my_unexpected_handler()

{

throw 1;

}

void f() throw(int)

{

throw 1L; // oops -- *bad* function

}

int main()

{

set_unexpected(my_unexpected_handler);

try

{

f();

}

catch (...)

{

}

return 0;

}

f()抛出了一个它承诺不抛的异常,于是my_unexpected_handler()被调用。这个处理函数没有任何办法来判断它被进入的原因。除了结束程序外,它唯一可能有些用的办法是抛出另外一个异常,希望新异常满足被老异常违背的异常规格申明,并且程序的其它部分将捕获这个新异常。

在这个例子里,my_unexpected_handler()抛出的int异常满足老异常违背的异常规格申明,并且main()成功地捕获了它。但稍作变化:

#include

using namespace std;

void my_unexpected_handler()

{

throw 1;

}

void f() throw(char)

{

throw 1L; // oops -- *bad* function

}

int main()

{

set_unexpected(my_unexepected_handler);

try

{

f();

}

catch (...)

{

}

return 0;

}

my_unexpected_handler()仍然在unexpected异常发生后被调用,并仍然抛出了一个int型异常。不幸的是,int型异常现在和老异常违背的异常规格申明相违背。因此,我们现在两次违背了同一异常规格申明:第一次是f(),第二次是f()的援助者my_unexpected_handler()

1.2 Terminate

现在,程序放弃了,并调用运行库的程序terminate()自毁。terminate()函数是标准运行库在异常处理上的最后一道防线。当程序的异常处理体系感到无望时,C++标准要求程序调用terminate()函数。C++标准的Subclause 15.5.1列出了调用terminate()的情况:

unexpected()处理函数一样,terminate()处理函数也可以用户定义。但和unexpected()处理函数不同的是,terminate()处理函数必须结束程序。记住:当你的terminate()处理函数被进入时,异常处理体系已经无效了,此是程序所需要的最后一件事是找一个terminate()处理函数来丢弃异常。

在能避免时就不要让你的程序调用terminate()terminate()其实是个叫得好听点的exit()。如果terminate()被调用了,你的程序就会以一种不愉快的方式死亡。

就如同不能完全支持unexpected()一样,Visual c++也不能完全支持terminate()。要在实际运行中验证的话,运行:

#include

#include

#include

using namespace std;

void my_terminate_handler()

{

printf("in my_terminate_handler ");

abort();

}

int main()

{

set_terminate(my_terminate_handler);

throw 1; // nobody catches this

return 0;

}

根据C++标准,抛出了一个没人捕获的异常将导致调用terminate()(这是我前面提到的Subclause 15.5.1中列举的情况之一)。于是,上面的程序一个输出:

in my_terminate_handler

但,用Visual C++编译并运行,程序没有输出任何东西。

1.3 避免terminate

在我们的unexpected()例子中,terminate()最终被调用是因为f()抛出了unexpected异常。我们的unexpected_handler()试图阻住这个不愉快的事,通过抛出一个新异常,但没成功;这个抛出行为因再度产生它试图解决的那个问题而结束。我们需要找到一个方法以使得unexpected()处理函数将控制权传给程序的其它部分(假定那部分程序是足够聪明的,能够成功处掉异常)而不导致程序终止。

很高兴,C++标准正好提供了这样一个方法。如我们所看过的,从unexpected()处理函数中抛出的异常对象必须符合(老异常违背的)异常规格申明。这个规则有一个例外:如果如果被违背的异常规格申明中包含类型bad_exception,一个bad_exception对象将替代unexpected()处理函数抛出的对象。例如:

#include

#include

using namespace std;

void my_unexpected_handler()

{

throw 1;

}

void f() throw(char, bad_exception)

{

throw 1L; // oops -- *bad* function

}

int main()

{

set_unexpected(my_unexpected_handler);

try

{

f();

}

catch (bad_exception const &)

{

printf("caught bad_exception ");

// ... even though such an exception was never thrown

}

return 0;

}

当用C++标准兼容的编译器编译并运行,程序输出:

caught bad_exception

当用Visual C++编译并运行,程序没输出任何东西。因为Visual c++并没有在第一次抛异常的地方捕获unexpected异常,它没有机会进行bad_exception的替换。

和前面的例子相同的是,f()仍然违背它的异常规格申明,而my_unexpected_handler()仍然抛出一个int。不同之处是:f()的异常规格申明包含bad_exception。结果,程序悄悄地将my_unexpected_handler()原来抛出的int对象替换为bad_exception对象。因为bad_exception异常是允许的,terminate()没有被调用,并且这个bad_exception异常能被程序的其它部分捕获。

最终结果:最初从f()抛出的long异常先被映射为int,再被映射为bad_exception。这样的映射不但避免了前面导致terminate的再次异常问题,还给程序的其它部分一个修正的机会。bad_exception异常对象的存在表明了某处最初抛出了一个unexpected异常。通过在问题点附近捕获这样的对象,程序可以得体地恢复。

我也注意到一个奇怪的地方。在代码里,你看到f()抛出了一个longmy_unexpected_handler()抛出了一个int,而没人抛出bad_exception,但main()确实捕获到一个bad_exception。是的,程序捕获了一个它从没抛出的对象。就我所知,唯一被允许发生这种行为的地方就是unexpected异常处理函数和bad_exception异常间的相互作用。

1.4 一个更特别的函数

C++标准定义了3个“特别”函数来捕获异常。其中,你已经看到了terminate()unexpected()。最后,也是最简单的一个是uncaght_exception()。摘自C++标准(15.5.3):

函数bool uncaught_exception()在被抛出的异常对象完成赋值到匹配的异常处理函数的异常申明完成初始化之间返回true。包括其中的退栈过程。如果异常被再次抛出,uncaught_exception() 从再抛点到再抛对象被再次捕获间返回true

uncaught_exception() 让你查看是否程序抛出了一个异常而还没有被捕获。这个函数对析构函数有特别意义:

#include

#include

using namespace std;

class X

{

public:

~X();

};

X::~X()

{

if (uncaught_exception())

printf("X::~X called during stack unwind ");

else

printf("X::~X called normally ");

}

int main()

{

X x1;

try

{

X x2;

throw 1;

}

catch (...)

{

}

return 0;

}

C++标准兼容的环境下,程序输出:

X::~X called during stack unwind

X::~X called normally

x1x2main()抛出异常前构造。退栈时调用x2的析构函数。因为一个未被捕获的异常在析构函数调用期间处于活动状态,uncaught_exception()返回true。然后,x1的析构函数被调用(在main()退出时),异常已经恢复,uncaught_exception()返回false

和以前一样,Visual C++在这里也不支持C++标准。在其下编译,程序输出:

X::~X called normally

X::~X called normally

如果你了解MicrosoftSEH(我在第二部分讲过的),就知道uncaught_exception()类似于SEHAbnormalTermination()。在它们各自的应用范围内,两个函数都是检测是否一个被抛出的异常处于活动状态而仍然没有被捕获。

1.5 小结

大多数函数不直接抛异常,但将其它函数抛的异常传递出来。决定哪些异常被传递是非常困难的,尤其是来自于没有异常规格申明的函数的。bad_exception ()是一个安全的阀门,提供了一个方法来保护那些你不能进行完全解析的异常。

这些保护能工作,但,和普通的异常处理函数一样,需要你明确地设计它。对每个可能违背其异常规格申明的函数,你都必须记得在其异常规格申明中加一个bad_exception并在某处捕获它。bad_exception和其它异常没有什么不同:如果你不想捕获它,不去产生它就行了。一个没有并捕获的bad_exception将导致程序终止,就象在最初的地方你没有使用bad_exception进行替换一样。

异常规格申明使你意图明确。它说“这是我允许这个函数抛出的异常的集合;如果函数抛出了其它东西,不是我的设计错了就是程序有神经病(the program is buggy)”。一个unexpected异常,不管它怎么出现的,都表明了一个逻辑错误。我建议你最好让错误以一种可预见的方式有限度地发生。

所有这些表明你可以描绘你的代码在最开始时的异常的行为。不幸的是,这样的描绘接近于巫术。下次,我将给出一些指导方针来分析你的代码中的异常。

没有评论: