1. 异常规格申明
现在是探索C++标准运行库和Visual C++在头文件<exception>中申明的异常支持的时候了。根据C++标准(subclause 18.6,“Exception handling” )上的描述,这个头文件申明了:
l 从运行库中抛出的异常对象的基类。
l 任何抛出的违背异常规格申明的对象的可能替代物。
l 在违背异常规格申明的异常被抛出是被调用的函数,以及在其行为上增加东西的钩子(“hook”)。
l 在异常处理过程被终止时被调用的函数,以及在其行为上增加东西的钩子。
我从分析异常规格申明及程序违背它时遭到什么可怕后果开始。分析将针对上面提到的主题,以及通常C++异常处理时的一些杂碎。
1.1 异常规格申明回顾
异常规格申明是C++函数申明的一部分,它们指定了函数可以抛出什么异常。例如,函数
void f1() throw(int)
可以抛出一个整型异常,而
void f2() throw(char *, E)
可以抛出一个char *或一个E(这里E是用户自定义类型)类型的异常。一个空的规格申明
void f3() throw()
表明函数不抛出异常,而没有规格申明
void f4()
表明函数可以抛出任何东西。注意语法
void f4() throw(...)
比前面的“抛任何东西”的函数更好,因为它类似“捕获任何东西”
catch(...)
然而,认可“抛任何东西” 的函数就允许了那些在异常规格申明存在前写下的函数。
1.2 违背异常规格申明
迄今为止,我写的都是:函数可能抛出在它的异常规格申明中描述的异常。“可能”有些单薄,“必须”则有力些。“可能”表示了函数可以忽略它们的异常规格。你也许认为编译器将禁止这种行为:
void f() throw() // Promises not to throw...
{
throw 1; // ...but does anyway - error?
}
但你错了。用Visual C++试一下,你将发现编译器保持沉默,它没有发现编译期错误。实际上,在我所用过的编译器中,没有一个报了编译期错误。
话虽这么说,但异常规格申明有它的规则的,函数违背它将遭受严重后果的。不幸的是,这些后果表现在运行期错误而不是编译期。想看的话,把上面的小段代码放到一个完整程序中:
void f() throw()
{
throw 1;
}
int main()
{
f();
return 0;
}
当程序运行时将发生什么?f()抛出一个int型异常,违背了它的契约。你可能认为这个异常将从main()中漏入运行期库。基于这个假设,你倾向于使用一个简单的try块:
#include <stdio.h>
void f() throw()
{
throw 1;
}
int main()
{
try
{
f();
}
catch (int)
{
printf("caught int\n");
}
return 0;
}
来捕获这个异常,以防止它漏出去。
实际上,如果你用Visual C++ 6编译并运行,你将得到:
caught int
你再次奇怪throw()异常规格实际做了什么有用的事,除了增加了源代码的大小和看起来比较快感。你的奇怪感觉将变得迟钝,只要一回想到前面说了多少Visual C++违背C++标准的地方,只不过再多一个新问题:Visaul C++正确地处理了违背异常规格申明的情况了吗?
1.3 调查说明……
没有!
这个程序的行为符合标准吗?catch语句不该进入的。来自于标准(subclauses 15.5.2 and 18.6.2.2):
l 一个异常规格申明保证只有被列出的异常被抛出。
l 如果带异常规格申明的函数抛出了一个没有列出的异常,函数
l void unexpected()在退完栈后立即被调用。
l 函数unexpected()将不会返回……
当一个函数试图抛出没有列出的异常时,通过unexpected()函数调用了一个异常处理函数。这个异常处理函数的默认实现是调用terminate() 来结束程序。
在我给你一个简短的例程后,我将展示Visual C++的行为怎么样地和标准不同。
1.4 unexpected()函数指南
unexpected()函数是标准运行库在头文件<exception>中申明的函数。和其它大部分运行库函数一样,unexpected()函数存在于命名空间std中。它不接受参数,也不返回任何东西,实际上unexpected()函数从不返回,就象abort()和exit()一样。如果一个函数违背了它自己的异常规格申明,unexpected()函数在退完栈后被立即调用。
基于我对标准的理解,运行库的unexpected()函数的实现理论上是这样的:
void _default_unexpected_handler_()
{
std::terminate();
}
std::unexpected_handler _unexpected_handler =
_default_unexpected_handler;
void unexpected()
{
_unexpected_handler();
}
(_default_unexpected_handler和_unexpected_handler是我虚构的名字。你的运行库的实现可能使用其它名称,完全取决于其实现。)
std::unexpected()调用一个函数来真正处理unexpected的异常。它通过一个隐藏的指针(_unexpected_handler,类型是std::unexpected_handler)来引用这个处理函数的。运行库提供了一个默认处理函数(default_unexpected_handler()),它调用std::terminate()来结束程序。
因为是通过指针_unexpected_handler间接调用的,你可以将内置的调用_default_unexpected_handler改为调用你自己的处理函数,只要这个处理函数的类型兼容于std::unexpected_handler:
typedef void (*unexpected_handler)();
同样,处理函数必须不返回到它的调用者(std::unexpected())中。没人阻止你写一个会返回的处理函数,但这样的处理函数不是标准兼容的,其结果是程序的行为有些病态。
你可以通过标准运行库的函数std::set_unexpected()来挂接自己的处理函数。注意,运行库只维护一个处理函数来处理所有的unexpected异常;一旦你调用了set_unexpected()函数,运行库将不再记得前一次的处理函数。(和atexit()比较一下,atexit()至少可以挂32重exit处理函数。)要克服这个限制,你要么在不同的时间设置不同的处理函数,要么使你的处理函数在不同的上下文时有不同的行为。
1.5 Visual C++ vs unexpected
试一下这个简单的例子:
#include <exception>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
void my_unexpected_handler()
{
printf("in unexpected handler\n");
abort();
}
void throw_unexpected_exception() throw(int)
{
throw 1L; // violates specification
}
int main()
{
set_unexpected(my_unexpected_handler);
throw_unexpected_exception();
printf("this line should never appear\n");
return 0;
}
用一个标准兼容的编译器编译并运行,程序结果是:
in unexpected handler
可能接下来是个异常异常终止的特殊(因为有abort()的调用)。但用Visual C++编译并运行,程序会抛出“Unhandled exception”对话框。关闭对话框后,程序输出:
this line should never appear
必须承认,Visual C++没有正确实现unexpected()。这个函数被申明在<exception>中,运行期库中有其实现,只不过这个实现不做任何事。
实际上,Visual C++甚至没有正确地申明,用这个理论上等价的程序可以证明:
#include <exception>
#include <stdio.h>
#include <stdlib.h>
//using namespace std;
void my_unexpected_handler()
{
printf("in unexpected handler\n");
abort();
}
void throw_unexpected_exception() throw(int)
{
throw 1L; // violates specification
}
int main()
{
std::set_unexpected(my_unexpected_handler);
throw_unexpected_exception();
printf("this line should never appear\n");
return 0;
}
Visual C++不能编译这个程序。查看<exception>表明:set_unexpected_handler()被申明为全局函数而不是在命名空间std中。实际上,所有的unexpected族函数都被申明为全局函数。
底线:Visual c++能编译使用unexpected()等函数的程序,但运行时的行为是不正确的。
我希望Microsoft能在下一版中改正这些问题。在未改正前,当讨论涉及到unexpected()时,我建议你使用标准兼容的C++编译器。
1.6 维持程序存活
在我所展示的简单例子中,程序在my_unexpected_handler()里停止了。有时,让程序停止是合理和正确的;但更多情况下,程序停止是太刺激了,尤其是当unexpected异常表明的是程序只轻微错误。
假定你想处理unexpected异常,并恢复程序,就象对大多数其它“正常”异常一样。因为unexpected()从不返回,程序恢复似乎不可能,除非你看了标准的subclause 15.5.2:
unexpected()不该返回,但它可以throw(或re-throw)一个异常。如果它抛出一个新异常,而这异常是异常规格申明允许的,搜索另外一个异常处理函数的行为在调用unexpected()的地方继续进行。
太好了!如果my_unexpected_handler()抛出一个允许的异常,程序就能从最初的违背异常规格申明的地方恢复了。在我们的例子里,最初的异常规格申明允许int型的异常。根据上面的说法,如果my_unexpected_handler抛出一个int异常,程序将能继续了。
基于这种猜测,试一下:
#include <exception>
#include <stdio.h>
void my_unexpected_handler()
{
printf("in unexpected handler\n");
throw 2; // allowed by original specification
//abort();
}
用标准兼容的编译器编译运行,程序输出:
in unexpected handler
program resumed
和期望相符。
抛出的int异常和其它异常一样顺调用链传递,并被第一个相匹配的异常处理函数捕获。在我们的例子里,程序的控制权从my_unexpected_handler()向std::unexpected()再向main()回退,并在main()中捕获异常。用这种方法,my_unexpected_handler()变成了一个异常转换器,将一个最初的“坏”的long型异常转换为一个“好”的int型异常。
结论:通过转换一个unexpected异常为expected异常,你能恢复程序的运行。
1.7 预告
下次,我将结束std::unexpected()的讨论:揭示在my_unexpected_handler()中抛异常的限制,探索运行库对这些限制的补救,并给出处理unexpected异常的通行指导原则。我也将开始讨论运行库函数std::terminate()的相关内容。
void throw_unexpected_exception() throw(int)
{
throw 1L; // violates specification
}
int main()
{
std::set_unexpected(my_unexpected_handler);
try
{
throw_unexpected_exception();
printf("this line should never appear\n");
}
catch (int)
{
printf("program resumed\n");
}
return 0;
}