1. C++异常和Visual C++ SEH的混合使用
我在Part2介绍了Structured Exception Handling(简称SEH)。在那时我就说过,SEH是window及其平台上的编译器专有的。它不是定义在ISO C++标准中的,使用它的程序将不能跨编译器移植。因为我注重于标准兼容和可移植性,所以我对将windows专有的SEH映射为ISO标准C++的exception handing(简称EH)很感兴趣。
同时,我不是SEH的专家。对它的了解绝大部分来自于本专栏前面的研究。当我考虑混合使用SEH与EH时,我猜想解决方法应该是困难的和不是显而易见的。这是它花了我两个星期的原因:我预料到需要额外的时间来研究和试验。
很高兴,我完全错了。我不知道的是Visual C++运行期库直接支持了绝大部分我所想要的东西。不用创造新的方法了,我可以展示你Visual C++已经支持了的东西,以及改造为所需要的东西的方法。基于这个目的,我将研究同一个例子的四个不同版本。
1.1 Version 1:定义一个转换函数
捆绑SEH和EH的方法分两步:
l 一个用户自定义的转换函数来捕获SEH的异常并将它映射为C++的异常。
l 一个Visual C++运行期库函数来安装这个转换函数
用户自定义的转换函数必要有如下形式:
void my_translator(unsigned code, EXCEPTION_POINTERS *info);
转换函数接受一个SEH异常(通过给定的异常code和info来定义的)。然后抛出一个C++异常,以此将传入的SEH异常映射为向外传的C++异常。这个C++异常将出现在原来的SEH异常发生点上并向外传播。
这个机制非常象std::set_terminate()和std::set_unexpected()。要安装转换函数,要调用Visual C++库函数_set_se_translator()。这个函数申明在头文件eh.h中:
typedef void (*_se_translator_function)(unsigned, EXCEPTION_POINTERS *);
_se_translator_function _set_se_translator(_se_translator_function);
它接受一个指向新转换函数的指针,返回上次安装的指针。一旦安装了一个转换函数,前一次的就丢失了;任何时候只有一个转换函数有效。(在多线程程序中,每个线程有一个独立的转换函数。)
如果还没安装过转换函数,第一次调用_set_se_translator()返回值可能是(也可能不是)NULL。也就是说,不能不分青红皂白就通过其返回的指针调用函数。很有趣的,如果返回值是NULL,而你又通过此NULL调用函数,将产生一个SEH异常,并且进入你刚刚安装的转换函数。
一个简单的例子:
#include <iostream>
using namespace std;
int main()
{
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (unsigned exception)
{
cout << "caught C++ exception " << hex << exception << endl;
}
return 0;
}
运行它的话,这个控制台程序将导致如此一个windows messagebox:
它是由于一个未被捕获的SEH异常传递到程序外面造成的。
现在,增加一个异常转换函数,并将Visual C++运行库设为使用这个转换函数:
#include <iostream>
using namespace std;
#include "windows.h"
static void my_translator(unsigned code, EXCEPTION_POINTERS *)
{
throw code;
}
int main()
{
_set_se_translator(my_translator);
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (unsigned exception)
{
cout << "caught C++ exception " << hex << exception << endl;
}
return 0;
}
再运行程序。现在将看到:
caught C++ exception c0000005
my_translator()截获了SEH异常,并转换为C++异常,其类型为unsigned,内容为SEH异常码(本例中为C0000005h,它是一个非法读取错误)。因为这个C++异常出现在原来的SEH异常发生点,也就说在try块中,所以被try块的异常处理函数捕获了。
1.2 Version 2:定义一个转换对象
上面的例子非常简单,将每个SEH异常转换为一个unsigned值。实际上,你可能需要一个比较复杂的异常对象:
#include <iostream>
using namespace std;
//#include "windows.h"
#include "structured_exception.h"
/*static void my_translator(unsigned code, EXCEPTION_POINTERS *)
{
throw code;
}*/
int main()
{
//_set_se_translator(my_translator);
structured_exception::install();
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (structured_exception const &exception)
{
cout << "caught C++ exception " << hex << exception.what()
<< " thrown from " << exception.where() << endl;
}
return 0;
}
这个例子抛出了一个用户自定义类型(structured_exception)的C++异常。为了让这个例子更具实际意义,也更方便阅读,我将structured_exception的申明放到了头文件structured_exception.h中:
#if !defined INC_structured_exception_
#define INC_structured_exception_
#include "windows.h"
class structured_exception
{
public:
structured_exception(EXCEPTION_POINTERS const &) throw();
static void install() throw();
unsigned what() const throw();
void const *where() const throw();
private:
void const *address_;
unsigned code_;
};
#endif // !defined INC_structured_exception_
其实现文件为:
#include "structured_exception.h"
#include "eh.h"
//
// ::
//
static void my_translator(unsigned, EXCEPTION_POINTERS *info)
{
throw structured_exception(*info);
}
//
// structured_exception::
//
structured_exception::structured_exception
(EXCEPTION_POINTERS const &info) throw()
{
EXCEPTION_RECORD const &exception = *(info.ExceptionRecord);
address_ = exception.ExceptionAddress;
code_ = exception.ExceptionCode;
}
void structured_exception::install() throw()
{
_set_se_translator(my_translator);
}
unsigned structured_exception::what() const throw()
{
return code_;
}
void const *structured_exception::where() const throw()
{
return address_;
}
这些函数的意义是:
l my_translator()是异常转换函数。我把它从main文件中移到这儿。于是,main文件不再需要包含windows.h了。
l install()将运行器库的全局转换函数设置为my_translator()。
l structured_exception的构造函数接收并解析SEH异常的信息。
l what()返回SEH异常的异常码。
l where()返回SEH异常发生的地点。注意,where()的返回类型是void const *,虽然C++标准不同意将代码地址转换为void指针。我只是重复了Micorsoft的用法,因为Visual C++运行库将地址存在了SEH异常的EXCEPTION_RECORD的一个void *成员中了。
编译并链接这三个文件。运行结果是:
caught C++ exception c0000005 thrown from 0040181D
(其中的代码地址值在你的系统上可能有所不同。)
1.3 Version 3:模仿C++标准运行库
在my_translator()中,所有的SEH异常映射为同样的structured_exception类型。这使得异常容易被捕获,因为它们匹配于我们的唯一的异常处理函数:
catch (structured_exception const &exception)
虽然捕获了异常,但我们没有办法事先知道异常的类型。唯一能做的是运行期查询,调用这个异常的what()成员:
catch (structured_exception const &exception)
{
switch (exception.what())
{
case EXCEPTION_ACCESS_VIOLATION:
// ...
case EXCEPTION_INT_DIVIDE_BY_ZERO:
// ...
case EXCEPTION_STACK_OVERFLOW:
// ...
// ...
}
这样的查询需要windows.h中的信息,以知道最初的SEH异常码的含意。这样的需求违背了structured_exception的抽象原则。此外,switch语句也经常违背了多态的原则。从用户代码的角度看,你通常应该用继承和模板来实现它。
C++标准运行库在这方面提供了一些指导。如我在Part3中勾画的,头文件<stdexcept>定义了一个异常类层次,std::exception是根结点。这个根类定义了虚成员what(),它返回一个编译器自定义的NTBS(C++标准中是“以NULL结束的字符串”)。每个继承类指定自己的what()的返回值。虽然C++标准没有规定这些值的内容,但我相信标准委员会打算用这个字符串来描述异常的类型或含意的。
根据这种精神,standard_exception的申明是:
#if !defined INC_structured_exception_
#define INC_structured_exception_
#include "eh.h"
#include "windows.h"
class structured_exception
{
public:
structured_exception(EXCEPTION_POINTERS const &) throw();
static void install() throw();
virtual char const *what() const throw();
void const *where() const throw();
private:
void const *address_;
//unsigned code_;
};
class access_violation : public structured_exception
{
public:
access_violation(EXCEPTION_POINTERS const &) throw();
virtual char const *what() const throw();
};
class divide_by_zero : public structured_exception
{
public:
divide_by_zero(EXCEPTION_POINTERS const &) throw();
virtual char const *what() const throw();
};
#endif // !defined INC_structured_exception_
实现是:
#include <exception>
using namespace std;
#include "structured_exception.h"
#include "windows.h"
//
// ::
//
static void my_translator(unsigned code, EXCEPTION_POINTERS *info)
{
switch (code)
{
case EXCEPTION_ACCESS_VIOLATION:
throw access_violation(*info);
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
throw divide_by_zero(*info);
break;
default:
throw structured_exception(*info);
break;
}
}
//
// structured_exception::
//
structured_exception::structured_exception
(EXCEPTION_POINTERS const &info) throw()
{
EXCEPTION_RECORD const &exception = *(info.ExceptionRecord);
address_ = exception.ExceptionAddress;
//code_ = exception.ExceptionCode;
}
void structured_exception::install() throw()
{
_set_se_translator(my_translator);
}
char const *structured_exception::what() const throw()
{
return "unspecified Structured Exception";
}
void const *structured_exception::where() const throw()
{
return address_;
}
//
// access_violation::
//
access_violation::access_violation
(EXCEPTION_POINTERS const &info) throw()
: structured_exception(info)
{
}
char const *access_violation::what() const throw()
{
return "access violation";
}
//
// divide_by_zero::
//
divide_by_zero::divide_by_zero
(EXCEPTION_POINTERS const &info) throw()
: structured_exception(info)
{
}
char const *divide_by_zero::what() const throw()
{
return "divide by zero";
}
注意:
l 那些本来在用户的异常处理函数中的switch语句,现在移到了my_translator()中。不再是将所有SEH异常映射为单个值(如version 1中)或单个类型的对象(version 2),现在的my_translator()将它们映射为多个类型的对象(取决于运行时的实际环境)。
l structured_exception成为了一个基类。我没有让它成为纯虚类,这是跟从了C++标准运行库的引导(std::exception是个实体类)。
l 我没有定义任何析构函数,因为编译器隐含提供的的析构函数对这些简单类足够了。如果我定义了析构函数,它们将需要定义为virtual。
l what()现在返回了一个用户友好的文本,取代了原来的SEH异常码。
l 因为我不再测试和显示这些代码,我去掉了数据成员code_。这使得structured_exception对象的大小减小了。(别太高兴:节省的空间又被新增的vptr指针抵销了,因为有了虚函数。)
l 因为模板方式更好,你应该放弃这种继承模式的。我将它留给你作为习题。
试一下新的方案,将main文件改为:
#include <iostream>
using namespace std;
#include "structured_exception.h"
int main()
{
structured_exception::install();
//
// discriminate exception by dynamic type
//
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (structured_exception const &exception)
{
cout << "caught " << exception.what() << endl;
}
//
// discriminate exception by static type
//
try
{
static volatile int i = 0;
i = 1 / i; // generate Structured Exception
}
catch (access_violation const &)
{
cout << "caught access violation" << endl;
}
catch (divide_by_zero const &)
{
cout << "caught divide by zero" << endl;
}
catch (structured_exception const &)
{
cout << "caught unspecified Structured Exception" << endl;
}
return 0;
}
再次运行,结果是:
caught access violation
caught divide by zero
1.4 Version 4:匹配于C++标准运行库
我们所有的standard_exception继承类都提供公有的成员
virtual char const *what() const;
来识别异常的动态类型。我不是随便选取的函数名:所有的C++标准运行库中的std::exception继承类为同样的目的提供了同样的公有成员。并且,what()是每个继承类的唯一的多态函数。
你可能已经注意到:
#include <exception>
class structured_exception : public std::exception
{
public:
structured_exception(EXCEPTION_POINTERS const &info) throw();
static void install() throw();
virtual char const *what() const throw();
void const *where() const throw();
private:
void const *address_;
};
因为structured_exception现在也是一个std:exception,我们可以用一个异常处理函数来同时捕获这个异常族:
catch (std::exception const &exception)
并且用同样的多态函数来获取异常的类型:
catch (std::exception const &exception)
{
cout << "caught " << exception.what();
}
用这样的方案,SEH异常能够表现得与标准C++的固有行为一致。同时,我们仍然能够特殊对待structured_exceptions并访问它的特殊成员:
catch (structured_exception const &exception)
{
cout << "caught Structured Exception from " << exception.where();
}
当然,如果你想放弃没有出现在std::exception继承体系中的类成员,如where(),你完全可以不使用基类structured_exception,而是直接从std::exception继承出access_violation等类。例如:一个divide-by-zero异常表示了一个程序值域控制错误,也就是说是个逻辑错误。你所以想直接从std::logic_error甚至是std::out_of_range派生devide_by_zero类。
我建议你看一下C++标准subclause 19.1 (“Exception classes”)以更好地理解C++标准运行库的异常继承体系,以及如何更好地将你的自定义异常熔入此继承体系。
1.5 总结束(略)