C++ 异常处理

来源:互联网 发布:手机erp工业软件 编辑:程序博客网 时间:2024/05/17 15:04

5. 异常

 C++新增的异常(exception)机制改变了某些事情,这种改变是深刻的,彻底的,可能是令人不舒服的。例如:使用未经处理的或原始的指针变得很危险;资源泄漏的可能性增加了;写出具有你希望的行为的构造函数与析构函数变得更加困难。特别小心防止程序执行时突然崩溃。执行程序和库程序尺寸增加了,同时运行速度降低了。

 这就使我们所知道的事情。很多使用C++的人都不知道在程序中使用异常,大多数人不知道如何正确使用它。在异常被抛出后,使软件的行为具有可预测性和可靠性,在众多方法中至今也没有一个一致的方法能做到这点。(为了深刻了解这个问题,参见Tom Cargill写的Exception Handling: A False Sense of Security。有关这些问题的进展情况的信息,参见Jack Reeves 写的Coping with Exceptions和Herb Sutter写的Exception-Safe Generic Containers。)

 我们知道:程序能够在存在异常的情况下正常运行是因为它们按照要求进行了设计,而不是因为巧合。异常安全(Exception-safe)的程序不是偶然建立的。一个没有按照要求进行设计的程序在存在异常的情况下运行正常的概率与一个没有按照多线程要求进行设计的程序在多线程的环境下运行正常的概率相同,概率为0。

 为什么使用异常呢?自从C语言被发明初来,C程序员就满足于使用错误代码(Error code),所以为什么还要弄来异常呢,特别是如果异常如我上面所说的那样存在着问题。答案是简单的:异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。


 C程序员能够仅通过setjmp和longjmp来完成与异常处理相似的功能。但是当longjmp在C++中使用时,它存在一些缺陷,当它调整堆栈时不能对局部对象调用析构函数。(WQ加注,VC++能保证这一点,但不要依赖这一点。)而大多数C++程序员依赖于这些析构函数的调用,所以setjmp和longjmp不能够替换异常处理。如果你需要一个方法,能够通知不可被忽略的异常状态,并且搜索栈空间(searching the stack)以便找到异常处理代码时,你还得确保局部对象的析构函数必须被调用,这时你就需要使用C++的异常处理。

 因为我们已经对使用异常处理的程序设计有了很多了解,下面这些条款仅是一个对于写出异常安全(Exception-safe)软件的不完整的指导。然而它们给任何在C++中使用异常处理的人介绍了一些重要思想。通过留意下面这些指导,你能够提高自己软件的正确性,强壮性和高效性,并且你将回避开许多在使用异常处理时经常遇到的问题。

使用析构函数防止资源泄漏
 对指针说再见。必须得承认:你永远都不会喜欢使用指针。

 Ok,你不用对所有的指针说再见,但是你需要对用来操纵局部资源(local resources)的指针说再见。假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的资料信息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate processing)。

 完成这个程序一个合理的方法是定义一个抽象类,ALA("Adorable Little Animal"),然后为小狗和小猫建立派生类。一个虚拟函数processAdoption分别对各个种类的动物进行处理:

 class ALA {
public:
 virtual void processAdoption() = 0;
 ...
};

class Puppy: public ALA {
public:
 virtual void processAdoption();
 ...

};

class Kitten: public ALA {
public:
 virtual void processAdoption();
 ...

};

 你需要一个函数从文件中读去信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。这个工作非常适合于虚拟构造器(virtual constructor),在条款M25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:
// 从s中读去动物信息, 然后返回一个指针
// 指向新建立的某种类型对象

ALA * readALA(istream& s);

 你的程序的关键部分就是这个函数,如下所示:
void processAdoptions(istream& dataSource)

{

 while (dataSource) { // 还有数据时,继续循环
 ALA *pa = readALA(dataSource); //得到下一个动物
 pa->processAdoption(); //处理收容动物
 delete pa; //删除readALA返回的对象

 }

}

 这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除pa。这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。

 现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?processAdoptions没有捕获异常,所以异常将传递给processAdoptions的调用者。传递中,processAdoptions函数中的调用pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。结果,任何时候pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏。

 堵塞泄漏很容易 :
void processAdoptions(istream& dataSource)
{
 while (dataSource) {

 ALA *pa = readALA(dataSource);
 try {
 pa->processAdoption();
 }

 catch (...) { // 捕获所有异常

 delete pa; // 避免内存泄漏
 // 当异常抛出时
 throw; // 传送异常给调用者

 }

 delete pa; // 避免资源泄漏

} // 当没有异常抛出时

}

 但是你必须用try和catch对你的代码进行小改动。更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须写两个delete代码。象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。不论我们是让processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢?

 (WQ加注,VC++支持try…catch…final结构的SEH。)

 我们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。因为当函数返回时局部对象总是被释放,无论函数是如何退出的。(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)

 具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like对象(类指针对象)被释放时,我们能让它的析构函数调用delete。替代指针的对象被称为smart pointers(灵巧指针),参见条款M28的解释,你能使得pointer-like对象非常灵巧。在这里,我们用不着这么聪明的指针,我们只需要一个pointer-lik对象,当它离开生存空间时知道删除它指向的对象。

 写出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的。每一个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是auto_ptr类的一些重要的部分:

template<class T>

class auto_ptr {

public:
 auto_ptr(T *p = 0): ptr(p) {} // 保存ptr,指向对象
 ~auto_ptr() { delete ptr; } // 删除ptr指向的对象
private:

 T *ptr; // raw ptr to object

};


 auto_ptr类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用。(我们至少必须加上拷贝构造函数,赋值operator和将在条款M28讲述的pointer-emulating函数),但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将不再为堆对象不能被删除而担心,即使在抛出异常时,对象也能被及时删除。(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对象数组的指针。如果想让auto_ptr类似于一个数组模板,你必须自己写一个。在这种情况下,用vector代替array可能更好。)

 使用auto_ptr对象代替raw指针,processAdoptions如下所示:

void processAdoptions(istream& dataSource)
{
 while (dataSource) {
 auto_ptr<ALA> pa(readALA(dataSource));
 pa->processAdoption();
 }

}

 这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。第一,pa被声明为一个auto_ptr<ALA>对象,而不是一个raw ALA*指针。第二,在循环的结尾没有delete语句。其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是不是很容易。

 隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息:

// 这个函数会发生资源泄漏,如果一个异常抛出
void displayInfo(const Information& info)

{

 WINDOW_HANDLE w(createWindow());
 在w对应的window中显式信息
 destroyWindow(w);
}
 很多window系统有C-like接口,使用象like createWindow 和destroyWindow函数来获取和释放window资源。如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢失,就象其它动态分配的资源一样。
 解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:


//一个类,获取和释放一个window 句柄

class WindowHandle {

public:
 WindowHandle(WINDOW_HANDLE handle): w(handle) {}
 ~WindowHandle() { destroyWindow(w); }
 operator WINDOW_HANDLE() { return w; } // see below

private:
 WINDOW_HANDLE w;
 // 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝
 WindowHandle(const WindowHandle&);
 WindowHandle& operator=(const WindowHandle&);
};

 这看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止,有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。(参见条款M5 ,了解为什么你应该谨慎使用隐式类型转换操作)

 通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:
// 如果一个异常被抛出,这个函数能避免资源泄漏
void displayInfo(const Information& info)

{

 WindowHandle w(createWindow());

 在w对应的window中显式信息;

}

 即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。
 资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例如当你正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析构函数需要特殊的技术。

在构造函数中防止资源泄漏

 如果你正在开发一个具有多媒体功能的通讯录程序。这个通讯录除了能存储通常的文字信息如姓名、地址、电话号码外,还能存储照片和声音(可以给出他们名字的正确发音)。 为了实现这个通信录,你可以这样设计:

class Image { // 用于图像数据

public:

 Image(const string& imageDataFileName);

 ...

};

 class AudioClip { // 用于声音数据

public:

 AudioClip(const string& audioDataFileName);

 ...

};

 

class PhoneNumber { ... }; // 用于存储电话号码

class BookEntry { // 通讯录中的条目

public:


 BookEntry(const string& name,

 const string& address = "",

 const string& imageFileName = "",

 const string& audioClipFileName = "");

 ~BookEntry();

// 通过这个函数加入电话号码

 void addPhoneNumber(const PhoneNumber& number);

...

private:

 string theName; // 人的姓名

 string theAddress; // 他们的地址

 list<PhoneNumber> thePhones; // 他的电话号码

 Image *theImage; // 他们的图像

 AudioClip *theAudioClip; // 他们的一段声音片段

};

 通讯录的每个条目都有姓名数据,所以你需要带有参数的构造函数,不过其它内容(地址、图像和声音的文件名)都是可选的。注意应该使用链表类(list)存储电话号码,这个类是标准C++类库(STL)中的一个容器类(container classes)。
 编写BookEntry 构造函数和析构函数,有一个简单的方法是:

BookEntry::BookEntry(const string& name,

 const string& address,

 const string& imageFileName,

 Const string& audioClipFileName)

: theName(name), theAddress(address),

 theImage(0), theAudioClip(0)

{

 if (imageFileName != "") {

 theImage = new Image(imageFileName);

 }

 if (audioClipFileName != "") {

 theAudioClip = new AudioClip(audioClipFileName);

 }

}

BookEntry::~BookEntry()

{

 delete theImage;

 delete theAudioClip;

}

 构造函数把指针theImage和theAudioClip初始化为空,然后如果其对应的构造函数参数不是空,就让这些指针指向真实的对象。析构函数负责删除这些指针,确保BookEntry对象不会发生资源泄漏。因为C++确保删除空指针是安全的,所以BookEntry的析构函数在删除指针前不需要检测这些指针是否指向了某些对象。

 看上去好像一切良好,在正常情况下确实不错,但是在非正常情况下(例如在有异常发生的情况下)它们恐怕就不会良好了。
 请想一下如果BookEntry的构造函数正在执行中,一个异常被抛出,会发生什么情况
呢?:

if (audioClipFileName != "") {

 theAudioClip = new AudioClip(audioClipFileName);

}

 一个异常被抛出,可以是因为operator new)不能给AudioClip分配足够的内存,也可以因为AudioClip的构造函数自己抛出一个异常。不论什么原因,如果在BookEntry构造函数内抛出异常,这个异常将传递到建立BookEntry对象的地方(在构造函数体的外面。 译者注)。

 现在假设建立theAudioClip对象建立时,一个异常被抛出(而且传递程序控制权到BookEntry构造函数的外面),那么谁来负责删除theImage已经指向的对象呢?答案显然应该是由BookEntry来做,但是这个想当然的答案是错的。~BookEntry()根本不会被调用,永远不会。

 C++仅仅能删除被完全构造的对象(fully contructed objects), 只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。所以如果一个BookEntry对象b做为局部对象建立,如下:

void testBookEntryClass()

{

 BookEntry b("Addison-Wesley Publishing Company",

 "One Jacob Way, Reading, MA 01867");

...

}

 并且在构造b的过程中,一个异常被抛出,b的析构函数不会被调用。而且如果你试图采取主动手段处理异常情况,即当异常发生时调用delete,如下所示:

void testBookEntryClass()

{

 BookEntry *pb = 0;

 try {

 pb = new BookEntry("Addison-Wesley Publishing Company",

 "One Jacob Way, Reading, MA 01867");

 ...

 }

 catch (...) { // 捕获所有异常

 delete pb; // 删除pb,当抛出异常时

 throw; // 传递异常给调用者

 }

 delete pb; // 正常删除pb

}

 你会发现在BookEntry构造函数里为Image分配的内存仍旧被丢失了,这是因为如果new操作没有成功完成,程序不会对pb进行赋值操作。如果BookEntry的构造函数抛出一个异常,pb将是一个空值,所以在catch块中删除它除了让你自己感觉良好以外没有任何作用。用灵巧指针(smart pointer)类auto_ptr<BookEntry>(参见条款M9)代替raw BookEntry*也不会也什么作用,因为new操作成功完成前,也没有对pb进行赋值操作。

 C++拒绝为没有完成构造操作的对象调用析构函数是有一些原因的,而不是故意为你制造困难。原因是:在很多情况下这么做是没有意义的,甚至是有害的。如果为没有完成构造操作的对象调用析构函数,析构函数如何去做呢?仅有的办法是在每个对象里加入一些字节来指示构造函数执行了多少步?然后让析构函数检测这些字节并判断该执行哪些操作。这样的记录会减慢析构函数的运行速度,并使得对象的尺寸变大。C++避免了这种开销,但是代价是不能自动地删除被部分构造的对象。

 因为当对象在构造中抛出异常后C++不负责清除对象,所以你必须重新设计你的构造函数以让它们自己清除。经常用的方法是捕获所有的异常,然后执行一些清除代码,最后再重新抛出异常让它继续转递。如下所示,在BookEntry构造函数中使用这个方法:

BookEntry::BookEntry(const string& name,

 const string& address,

 const string& imageFileName,

 const string& audioClipFileName)

: theName(name), theAddress(address),

 theImage(0), theAudioClip(0)

{

 try { // 这try block是新加入的

 if (imageFileName != "") {

 theImage = new Image(imageFileName);

 }

 if (audioClipFileName != "") {

 theAudioClip = new AudioClip(audioClipFileName);

 }

 }

 catch (...) { // 捕获所有异常

 delete theImage; // 完成必要的清除代码

 delete theAudioClip;

 throw; // 继续传递异常

 }

}


 不用为BookEntry中的非指针数据成员操心,在类的构造函数被调用之前数据成员就被自动地初始化。所以如果BookEntry构造函数体开始执行,对象的theName, theAddress 和 thePhones数据成员已经被完全构造好了。这些数据可以被看做是完全构造的对象,所以它们将被自动释放,不用你介入操作。当然如果这些对象的构造函数调用可能会抛出异常的函数,那么那些构造函数必须自己去考虑捕获异常并在继续传递这些异常之前完成必需的清除操作。

 你可能已经注意到BookEntry构造函数的catch块中的语句与在BookEntry的析构函数的语句几乎一样。这里的代码重复是绝对不可容忍的,所以最好的方法是把通用代码移入一个私有helper function中,让构造函数与析构函数都调用它。

class BookEntry {

public:

 ... // 同上

private:

 ...

 void cleanup(); // 通用清除代码

};

void BookEntry::cleanup()

{

 delete theImage;

 delete theAudioClip;

}

BookEntry::BookEntry(const string& name,

 const string& address,

 const string& imageFileName,

 const string& audioClipFileName)

: theName(name), theAddress(address),

 theImage(0), theAudioClip(0)

{

 try {

 ... // 同上

 }

 catch (...) {

 cleanup(); // 释放资源

 throw; // 传递异常


 }

}

BookEntry::~BookEntry()

{

 cleanup();

}

 这似乎行了,但是它没有考虑到下面这种情况。假设我们略微改动一下设计,让
theImage 和theAudioClip是常量(constant)指针类型:

class BookEntry {

public:

 ... // 同上

private:

 ...

 Image * const theImage; // 指针现在是

 AudioClip * const theAudioClip; // const类型

};

 必须通过BookEntry构造函数的成员初始化表来初始化这样的指针,因为再也没有其它地方可以给const指针赋值(参见Effective C++条款12)。通常会这样初始化theImage和theAudioClip:

// 一个可能在异常抛出时导致资源泄漏的实现方法

BookEntry::BookEntry(const string& name,

 const string& address,

 const string& imageFileName,

 const string& audioClipFileName)

: theName(name), theAddress(address),

 theImage(imageFileName != ""

 ? new Image(imageFileName)

 : 0),

 theAudioClip(audioClipFileName != ""

 ? new AudioClip(audioClipFileName)

 : 0)

{}

 这样做导致我们原先一直想避免的问题重新出现:如果theAudioClip初始化时一个异常被抛出,theImage所指的对象不会被释放。而且我们不能通过在构造函数中增加try和catch 语句来解决问题,因为try和catch是语句,而成员初始化表仅允许有表达式(这也是为什么我们必须在 theImage 和 theAudioClip的初始化中使用?:以代替if-then-else的原因)。

 无论如何,在异常传递之前完成清除工作的唯一的方法就是捕获这些异常,所以如果我们不能在成员初始化表中放入try和catch语句,我们把它们移到其它地方。一种可能是在私有成员函数中,用这些函数返回指针指向初始化过的theImage 和 theAudioClip对象。

class BookEntry {

public:

 ... // 同上

private:

 ... // 数据成员同上

Image * initImage(const string& imageFileName);

 AudioClip * initAudioClip(const string&

 audioClipFileName);

};

BookEntry::BookEntry(const string& name,

 const string& address,

 const string& imageFileName,

 const string& audioClipFileName)

: theName(name), theAddress(address),

 theImage(initImage(imageFileName)),

 theAudioClip(initAudioClip(audioClipFileName))

{}

// theImage 被首先初始化,所以即使这个初始化失败也
// 不用担心资源泄漏,这个函数不用进行异常处理。

Image * BookEntry::initImage(const string& imageFileName)

{

 if (imageFileName != "") return new Image(imageFileName);

 else return 0;

}

// theAudioClip被第二个初始化, 所以如果在theAudioClip
// 初始化过程中抛出异常,它必须确保theImage的资源被释放。
// 因此这个函数使用try...catch 。

AudioClip * BookEntry::initAudioClip(const string&


 audioClipFileName)

{

 try {

 if (audioClipFileName != "") {

 return new AudioClip(audioClipFileName);

 }

 else return 0;

 }

 catch (...) {

 delete theImage;

 throw;

 }

}

 上面的程序的确不错,也解决了令我们头疼不已的问题。不过也有缺点,在原则上应该属于构造函数的代码却分散在几个函数里,这令我们很难维护。

 更好的解决方法是采用条款M9的建议,把theImage 和 theAudioClip指向的对象做为一个资源,被一些局部对象管理。这个解决方法建立在这样一个事实基础上:theImage 和theAudioClip是两个指针,指向动态分配的对象,因此当指针消失的时候,这些对象应该被删除。auto_ptr类就是基于这个目的而设计的。(参见条款M9)因此我们把theImage 和 theAudioClip raw指针类型改成对应的auto_ptr类型。

class BookEntry {

public:

 ... // 同上

private:

 ...

 const auto_ptr<Image> theImage; // 它们现在是
 const auto_ptr<AudioClip> theAudioClip; // auto_ptr对象

};

 这样做使得BookEntry的构造函数即使在存在异常的情况下也能做到不泄漏资源,而且让我们能够使用成员初始化表来初始化theImage 和 theAudioClip,如下所示:

BookEntry::BookEntry(const string& name,

 const string& address,

 const string& imageFileName,

 const string& audioClipFileName)


: theName(name), theAddress(address),

 theImage(imageFileName != ""

 ? new Image(imageFileName)

 : 0),

 theAudioClip(audioClipFileName != ""

 ? new AudioClip(audioClipFileName)

 : 0)

{}

 在这里,如果在初始化theAudioClip时抛出异常,theImage已经是一个被完全构造的对象,所以它能被自动删除掉,就象theName, theAddress和thePhones一样。而且因为theImage 和 theAudioClip现在是包含在BookEntry中的对象,当BookEntry被删除时它们能被自动地删除。因此不需要手工删除它们所指向的对象。可以这样简化BookEntry的析构函数:

BookEntry::~BookEntry()

{} // nothing to do!

 这表示你能完全去掉BookEntry的析构函数。
 综上所述,如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造函数在存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值。

 在对象构造中,处理各种抛出异常的可能,是一个棘手的问题,但是auto_ptr(或者类似于auto_ptr的类)能化繁为简。它不仅把令人不好理解的代码隐藏起来,而且使得程序在面对异常的情况下也能保持正常运行。

禁止异常信息(exceptions)传递到析构函数外
 在有两种情况下会调用析构函数。第一种是在正常情况下删除一个对象,例如对象超出了作用域或被显式地delete。第二种是异常传递的堆栈辗转开解(stack-unwinding)过程中,由异常处理系统删除一个对象。

 在上述两种情况下,调用析构函数时异常可能处于激活状态也可能没有处于激活状态。遗憾的是没有办法在析构函数内部区分出这两种情况。因此在写析构函数时你必须保守地假设有异常被激活。因为如果在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用terminate函数。这个函数的作用正如其名字所表示的:它终止你程序的运行,而且是立即终止,甚至连局部对象都没有被释放。

 下面举一个例子,一个Session类用来跟踪在线计算机的sessions,session就是运行在从你一登录计算机开始一直到注销出系统为止的这段期间的某种东西。每个Session对象关注的是它建立与释放的日期和时间:

class Session {

public:

 Session();

 ~Session();

 ...

private:

 static void logCreation(Session *objAddr);

 static void logDestruction(Session *objAddr);

};

 函数logCreation 和 logDestruction被分别用于记录对象的建立与释放。我们因此可以这样编写Session的析构函数:

Session::~Session()

{

 logDestruction(this);

}

 一切看上去很好,但是如果logDestruction抛出一个异常,会发生什么事呢?异常没有被Session的析构函数捕获住,所以它被传递到析构函数的调用者那里。但是如果析构函数本身的调用就是源自于某些其它异常的抛出,那么terminate函数将被自动调用,彻底终止你的程序。这不是你所希望发生的事情。程序没有记录下释放对象的信息,这是不幸的,甚至是一个大麻烦。那么事态果真严重到了必须终止程序运行的地步了么?如果没有,你必须防止在logDestruction内抛出的异常传递到Session析构函数的外面。唯一的方法是用try和catch blocks。一种很自然的做法会这样编写函数:

Session::~Session()

{

 try {

 logDestruction(this);

 }

 catch (...) {

 cerr << "Unable to log destruction of Session object "

 << "at address "

 << this

 << "./n";

 }


}

 但是这样做并不比你原来的代码安全。如果在catch中调用operator<<时导致一个异常被抛出,我们就又遇到了老问题,一个异常被转递到Session析构函数的外面。  我们可以在catch中放入try,但是这总得有一个限度,否则会陷入循环。因此我们在释放Session时必须忽略掉所有它抛出的异常:

Session::~Session()

{

 try {

 logDestruction(this);

 }

 catch (...) { }

}

 catch表面上好像没有做任何事情,这是一个假象,实际上它阻止了任何从logDestruction抛出的异常被传递到session析构函数的外面。我们现在能高枕无忧了,无论session对象是不是在堆栈退栈(stack unwinding)中被释放,terminate函数都不会被调用。

 不允许异常传递到析构函数外面还有第二个原因。如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数就不会完全运行(它会停在抛出异常的那个地方上)。如果析构函数不完全运行,它就无法完成希望它做的所有事情。例如,我们对session类做一个修改,在建立session时启动一个数据库事务(database transaction),终止session时结束这个事务:

Session::Session() // 为了简单起见,,

{ // 这个构造函数没有

 // 处理异常

 logCreation(this);

 startTransaction(); // 启动 database transaction

}

Session::~Session()

{

 logDestruction(this);

 endTransaction(); // 结束database transaction

}

 如果在这里logDestruction抛出一个异常,在session构造函数内启动的transaction就没有被终止。我们也许能够通过重新调整session析构函数内的函数调用顺序来消除问题,但是如果endTransaction也抛出一个异常,我们除了回到使用try和catch外,别无选择。

 综上所述,我们知道禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。(如果你仍旧不很信服我所说的理由,可以去看Herb Sutter的文章Exception-Safe Generic Containers ,特别是“Destructors That Throw and Why They’re Evil”这段)。

:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
 从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别:
class Widget { ... }; //一个类,具体是什么类

 // 在这里并不重要

void f1(Widget w); // 一些函数,其参数分别为

void f2(Widget& w); // Widget, Widget&,或

void f3(const Widget& w); // Widget* 类型

void f4(Widget *pw);

void f5(const Widget *pw);

catch (Widget w) ... //一些catch 子句,用来

catch (Widget& w) ... //捕获异常,异常的类型为

catch (const Widget& w) ... // Widget, Widget&, 或

catch (Widget *pw) ... // Widget*

catch (const Widget *pw) ...

 你因此可能会认为用throw抛出一个异常到catch子句中与通过函数调用传递一个参数两者基本相同。这里面确有一些相同点,但是他们也存在着巨大的差异。
 让我们先从相同点谈起。你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。

 有这样一个函数,参数类型是Widget,并抛出一个Widget类型的异常:

// 一个函数,从流中读值到Widget中

istream operator>>(istream& s, Widget& w);

void passAndThrowWidget()

{

 Widget localWidget;


 cin >> localWidget; //传递localWidget到 operator>>

 throw localWidget; // 抛出localWidget异常

}

 当传递localWidget到函数operator>>里,不用进行拷贝操作,而是把operator>>内的引用类型变量w指向localWidget,任何对w的操作实际上都施加到localWidget上。这与抛出localWidget异常有很大不同。不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行lcalWidget的拷贝操作,也就说传递到catch子句中的是localWidget的拷贝。必须这么做,因为当localWidget离开了生存空间后,其析构函数将被调用。如果把localWidget本身(而不是它的拷贝)传递给catch子句,这个子句接收到的只是一个被析构了的Widget,一个Widget的“尸体”。这是无法使用的。因此C++规范要求被做为异常抛出的对象必须被复制。

 即使被抛出的对象不会被释放,也会进行拷贝操作。例如如果passAndThrowWidget函数声明localWidget为静态变量(static),

void passAndThrowWidget()

{

 static Widget localWidget; // 现在是静态变量(static);

 //一直存在至程序结束

 cin >> localWidget; // 象以前那样运行

 throw localWidget; // 仍将对localWidget

} //进行拷贝操作

 当抛出异常时仍将复制出localWidget的一个拷贝。这表示即使通过引用来捕获异常,也不能在catch块中修改localWidget;仅仅能修改localWidget的拷贝。对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行速度比参数传递要慢。

 当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。比如以下这经过少许修改的passAndThrowWidget:

class Widget { ... };

class SpecialWidget: public Widget { ... };

void passAndThrowWidget()

{

 SpecialWidget localSpecialWidget;

 ...

 Widget& rw = localSpecialWidget; // rw 引用SpecialWidget


 throw rw; //它抛出一个类型为Widget

 // 的异常

}

 这里抛出的异常对象是Widget,即使rw引用的是一个SpecialWidget。因为rw的静态类型(static type)是Widget,而不是SpecialWidget。你的编译器根本没有注意到rw引用的是一个SpecialWidget。编译器所注意的是rw的静态类型(static type)。这种行为可能与你所期待的不一样,但是这与在其他情况下C++中拷贝构造函数的行为是一致的。(不过有一种技术可以让你根据对象的动态类型dynamic type进行拷贝)
 异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样:

catch (Widget& w) // 捕获Widget异常
{
 ... // 处理异常
 throw; // 重新抛出异常,让它

} // 继续传递

catch (Widget& w) // 捕获Widget异常

{

 ... // 处理异常

 throw w; // 传递被捕获异常的

} // 拷贝

 这两个catch块的差别在于第一个catch块中重新抛出的是当前捕获的异常,而第二个catch块中重新抛出的是当前捕获异常的一个新的拷贝。如果忽略生成额外拷贝的系统开销,这两种方法还有差异么?

 当然有。第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。特别是如果这个异常开始就是做为SpecialWidget类型抛出的,那么第一个块中传递出去的还是SpecialWidget异常,即使w的静态类型(static type)是Widget。这是因为重新抛出异常时没有进行拷贝操作。第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型(static type)是Widget。一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。(顺便说一句,异常生成的拷贝是一个临时对象。正如条款19解释的,临时对象能让编译器优化它的生存期(optimize it out of existence),不过我想你的编译器很难这么做,
因为程序中很少发生异常,所以编译器厂商不会在这方面花大量的精力。)

 让我们测试一下下面这三种用来捕获Widget异常的catch子句,异常是做为
passAndThrowWidgetp抛出的:

catch (Widget w) ... // 通过传值捕获异常

catch (Widget& w) ... // 通过传递引用捕获

 // 异常

catch (const Widget& w) ... //通过传递指向const的引用

 //捕获异常

 我们立刻注意到了传递参数与传递异常的另一个差异。一个被异常抛出的对象(刚才解释过,总是一个临时对象)可以通过普通的引用捕获;它不需要通过指向const对象的引用(reference-to-const)捕获。在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里,但是在异常中却被允许。

 让我们先不管这个差异,回到异常对象拷贝的测试上来。我们知道当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝(参见Effective C++ 条款22),并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的。当我们这样声明一个catch子句时:

catch (Widget w) ... // 通过传值捕获

 会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中(WQ加注,重要:是两个!)。同样,当我们通过引用捕获异常时,
catch (Widget& w) ... // 通过引用捕获
catch (const Widget& w) ... //也通过引用捕获

 这仍旧会建立一个被抛出对象的拷贝:拷贝同样是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。

 我们还没有讨论通过指针抛出异常的情况。不过,通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。但,你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。
Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。(WQ加注,也就是说:必须是全局的或堆中的。)
 对象从函数的调用处传递到函数参数里与从异常抛出点传递到catch子句里所采用的方法不同,这只是参数传递与异常传递的区别的一个方面;第二个差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。比如在标准数学库(the standard math library)中sqrt函数:

double sqrt(double); // from <cmath> or <math.h>

 我们能这样计算一个整数的平方根,如下所示:
int i;

double sqrtOfi = sqrt(i);

 毫无疑问,C++允许进行从int到double的隐式类型转换,所以在sqrt的调用中,i 被悄悄地转变为double类型,并且其返回值也是double。一般来说,catch子句匹配异常类型时不会进行这样的转换。见下面的代码:

void f(int value)

{

 try {

 if (someFunction()) { // 如果 someFunction()返回
 throw value; //真,抛出一个整形值

 ...

 }

 }

 catch (double d) { // 只处理double类型的异常
 ...

 }
 ...
}

 在try块中抛出的int异常不会被处理double异常的catch子句捕获。该子句只能捕获类型真真正正为double的异常,不进行类型转换。因此如果要想捕获int异常,必须使用带有int或int&参数的catch子句。
 不过在catch子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异常。例如在标准C++库(STL)定义的异常类层次中的诊断部分(diagnostics portion )。

 捕获runtime_errors异常的Catch子句可以捕获range_error类型和overflow_error类型的异常;可以接收根类exception异常的catch子句能捕获其任意派生类异常。
 这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上:

catch (runtime_error) ... // can catch errors of type

catch (runtime_error&) ... // runtime_error,

catch (const runtime_error&) ... // range_error, or

 // overflow_error

catch (runtime_error*) ... // can catch errors of type

catch (const runtime_error*) ... // runtime_error*,


 // range_error*, or

 // overflow_error*

 第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常:
catch (const void*) ... //捕获任何指针类型异常

 传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的catch子句捕获,即使同时存在有能直接处理该派生类异常的catch子句,与相同的try块相对应。例如:

try {

 ...

}

catch (logic_error& ex) { // 这个catch块 将捕获

 ... // 所有的logic_error

} // 异常, 包括它的派生类

 

catch (invalid_argument& ex) { // 这个块永远不会被执行

 ... //因为所有的

} // invalid_argument

 // 异常 都被上面的

 // catch子句捕获。

 与上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里。你可以这样说虚拟函数采用最优适合法,而异常处理采用的是最先适合法。如果一个处理派生类异常的catch子句位于处理基类异常的catch子句后面,编译器会发出警告。(因为这样的代码在C++里通常是不合法的。)不过你最好做好预先防范:不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面。象上面那个例子,应该这样去写:

try {

 ...

}

catch (invalid_argument& ex) { // 处理 invalid_argument

 ... //异常

}

catch (logic_error& ex) { // 处理所有其它的

 ... // logic_errors异常


}

 综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝。第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。

5.5 Item M13:通过引用(reference)捕获异常


 当你写一个catch子句时,必须确定让异常通过何种方式传递到catch子句里。你可以有三个选择:与你给函数传递参数一样,通过指针(by pointer),通过传值(by value)或通过引用(by reference)。

 我们首先讨论通过指针方式捕获异常(catch by pointer)。从throw处传递一个异常到catch子句是一个缓慢的过程,在理论上这种方法的实现对于这个过程来说是效率最高的。因为在传递异常信息时,只有采用通过指针抛出异常的方法才能够做到不拷贝对象,例如:

class exception { ... }; // 来自标准C++库(STL)
 // 中的异常类层次
 // (参见条款12)
void someFunction()
{
 static exception ex; // 异常对象
 ...
 throw &ex; // 抛出一个指针,指向ex
 ...
}
void doSomething()
{
 try {
 someFunction(); // 抛出一个 exception*
 }
 catch (exception *ex) { // 捕获 exception*;
 ... // 没有对象被拷贝
 }
}
 这看上去很不错,但是实际情况却不是这样。为了能让程序正常运行,程序员定义异常对象时必须确保当程序控制权离开抛出指针的函数后,对象还能够继续生存。全局与静态对象都能够做到这一点,但是程序员很容易忘记这个约束。如果真是如此的话,他们会这样写代码:

void someFunction()
{
 exception ex; // 局部异常对象;
 // 当退出函数的生存空间时
 // 这个对象将被释放。
 ...

 throw &ex; // 抛出一个指针,指向
 ... // 已被释放的对象

}

 这简直糟糕透了,因为处理这个异常的catch子句接受到的指针,其指向的对象已经不再存在。

 另一种抛出指针的方法是建立一个堆对象(new heap object):
void someFunction()

{

 ...

 throw new exception; // 抛出一个指针,指向一个在堆中
 ... // 建立的对象(希望

} // 操作符new — 参见条款M8—
 // 自己不要再抛出一个
 // 异常!)

 这避免了捕获一个指向已被释放对象的指针的问题,但是catch子句的作者又面临一个令人头疼的问题:他们是否应该删除他们接受的指针?如果是在堆中建立的异常对象,那他们必须删除它,否则会造成资源泄漏。如果不是在堆中建立的异常对象,他们绝对不能删除它,否则程序的行为将不可预测。该如何做呢?

 这是不可能知道的。一些被调用者可能会传递全局或静态对象的地址,另一些可能传递堆中建立的异常对象的地址。通过指针捕获异常,将遇到一个哈姆雷特式的难题:是删除还是不删除?这是一个难以回答的问题。所以你最好避开它。

 而且,通过指针捕获异常也不符合C++语言本身的规范。四个标准的异常――
bad_alloc(当operator new(参见条款M8)不能分配足够的内存时,被抛出),bad_cast(当dynamic_cast针对一个引用(reference)操作失败时,被抛出),bad_typeid(当dynamic_cast对空指针进行操作时,被抛出)和bad_exception(用于unexpected异常;参见条款M14)――都不是指向对象的指针,所以你必须通过值或引用来捕获它们。

 通过值捕获异常(catch-by-value)可以解决上述的问题,例如异常对象删除的问题和使用标准异常类型的问题。但是当它们被抛出时系统将对异常对象拷贝两次。而且它会产生slicing problem,即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。这样的sliced对象实际上是一个基类对象:它们没有派生类的数据成员,而且当本准备调用它们的虚拟函数时,系统解析后调用的却是基类对象的函数。(当一个对象通过传值方式传递给函数,也会发生一样的情况)。例如下面这个程序采用了扩展自标准异常类的异常类层次体系:

class exception { // 如上,这是

public: // 一个标准异常类

 virtual const char * what() throw();

 // 返回异常的简短描述.

 ... // (在函数声明的结尾处

 // 的"throw()",

};
class runtime_error: //也来自标准C++异常类

 public exception { ... };

class Validation_error: // 客户自己加入个类

 public runtime_error {

public:

 virtual const char * what() throw();

 // 重新定义在异常类中

 ... //虚拟函数

}; //

void someFunction() // 抛出一个 validation

{ // 异常

 ...

 if (a validation 测试失败) {

 throw Validation_error();


 }

...

}

void doSomething()

{

 try {

 someFunction(); // 抛出 validation

 } //异常

 catch (exception ex) { //捕获所有标准异常类

 // 或它的派生类

 cerr << ex.what(); // 调用 exception::what(),

 ... // 而不是Validation_error::what()

}

}

 调用的是基类的what函数,即使被抛出的异常对象是runtime_error或 Validation_error类型并且它们已经重新定义了这个虚拟函数。这种slicing行为绝不是你所期望的。

 最后剩下方法就是通过引用捕获异常(catch-by-reference)。通过引用捕获异常能使你避开上述所有问题。不象通过指针捕获异常,这种方法不会有对象删除的问题而且也能捕获标准异常类型。也不象通过值捕获异常,这种方法没有slicing problem,而且异常对象只被拷贝一次。

 我们采用通过引用捕获异常的方法重写最后那个例子,如下所示:
void someFunction() //这个函数没有改变

{

 ...

 if (a validation 测试失败) {

 throw Validation_error();

 }

 ...

}

void doSomething()

{

 try {

 someFunction(); // 没有改变


 }

 catch (exception& ex) { // 这里,我们通过引用捕获异常
 // 以替代原来的通过值捕获
 cerr << ex.what(); // 现在调用的是

 // Validation_error::what(),

 ... // 而不是 exception::what()

 }

}

 这里没有对throw进行任何改变,仅仅改变了catch子句,给它加了一个&符号。然而这个微小的改变能造成了巨大的变化,因为catch块中的虚拟函数能够如我们所愿那样工作了:调用的Validation_erro函数是我们重新定义过的函数。
 如果你通过引用捕获异常(catch by reference),你就能避开上述所有问题,不会为是否删除异常对象而烦恼;能够避开slicing异常对象;能够捕获标准异常类型;减少异常对象需要被拷贝的数目。所以你还在等什么?通过引用捕获异常吧(Catch exceptions by reference)!

审慎使用异常规格(exception specifications)
 毫无疑问,异常规格是一个引人注目的特性。它使得代码更容易理解,因为它明确地描述了一个函数可以抛出什么样的异常。但是它不只是一个有趣的注释。编译器在编译时有时能够检测到异常规格的不一致。而且如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。异常规格既可以做为一个指导性文档同时也是异常使用的强制约束机制,它好像有着很诱人的外表。

 不过在通常情况下,美貌只是一层皮,外表的美丽并不代表其内在的素质。函数unexpected缺省的行为是调用函数terminate,而terminate缺省的行为是调用函数abort,所以一个违反异常规格的程序其缺省的行为就是halt(停止运行)。在激活的栈中的局部变量没有被释放,因为abort在关闭程序时不进行这样的清除操作。对异常规格的触犯变成了一场并不应该发生的灾难。

 不幸的是,我们很容易就能够编写出导致发生这种灾难的函数。编译器仅仅部分地检测异常的使用是否与异常规格保持一致。一个函数调用了另一个函数,并且后者可能抛出一个违反前者异常规格的异常,(A函数调用B函数,但因为B函数可能抛出一个不在A函数异常规格之内的异常,所以这个函数调用就违反了A函数的异常规格 译者注)编译器不对此种情况进行检测,并且语言标准也禁止编译器拒绝这种调用方式(尽管可以显示警告信息)。

 例如函数f1没有声明异常规格,这样的函数就可以抛出任意种类的异常:
extern void f1(); // 可以抛出任意的异常
 假设有一个函数f2通过它的异常规格来声明其只能抛出int类型的异常:
void f2() throw(int);
f2调用f1是非常合法的,即使f1可能抛出一个违反f2异常规格的异常:
void f2() throw(int)
{
 ...
 f1(); // 即使f1可能抛出不是int类型的
 //异常,这也是合法的。
 ...
}
 当带有异常规格的新代码与没有异常规格的老代码整合在一起工作时,这种灵活性就显得很重要。
 因为你的编译器允许你调用一个函数,其抛出的异常与发出调用的函数的异常规格不一致,并且这样的调用可能导致你的程序执行被终止,所以在编写软件时应该采取措施把这种不一致减小到最少。一种好方法是避免在带有类型参数的模板内使用异常规格。例如下面这种模板,它好像不能抛出任何异常:

// a poorly designed template wrt exception specifications
template<class T>
bool operator==(const T& lhs, const T& rhs) throw()
{
 return &lhs == &rhs;
}

 这个模板为所有类型定义了一个操作符函数operator==。对于任意一对类型相同的对象,如果对象有一样的地址,该函数返回true,否则返回false。
 这个模板包含的异常规格表示模板生成的函数不能抛出异常。但是事实可能不会这样,因为opertor&能被一些类型对象重载。如果被重载的话,当调用从operator==函数内部调用opertor&时,opertor&可能会抛出一个异常,这样就违反了我们的异常规格,使得程序控制跳转到unexpected。

 上述的例子是一种更一般问题的特例,这个更一般问题也就是没有办法知道某种模板类型参数抛出什么样的异常。我们几乎不可能为一个模板提供一个有意义的异常规格。因为模板总是采用不同的方法使用类型参数。解决方法只能是模板和异常规格不要混合使用。

 能够避免调用unexpected函数的第二个方法是如果在一个函数内调用其它没有异常规格的函数时应该去除这个函数的异常规格。这很容易理解,但是实际中容易被忽略。比如允

许用户注册一个回调函数:
// 一个window系统回调函数指针
//当一个window系统事件发生时
typedef void (*CallBackPtr)(int eventXLocation,

 int eventYLocation,

 void *dataToPassBack);

//window系统类,含有回调函数指针,

//该回调函数能被window系统客户注册

class CallBack {

public:

 CallBack(CallBackPtr fPtr, void *dataToPassBack)

 : func(fPtr), data(dataToPassBack) {}

 void makeCallBack(int eventXLocation,

 int eventYLocation) const throw();

private:

 CallBackPtr func; // function to call when

 // callback is made

 void *data; // data to pass to callback

}; // function

// 为了实现回调函数,我们调用注册函数,
//事件的作标与注册数据做为函数参数。

void CallBack::makeCallBack(int eventXLocation,

 int eventYLocation) const throw()

{

 func(eventXLocation, eventYLocation, data);

}

 这里在makeCallBack内调用func,要冒违反异常规格的风险,因为无法知道func会抛出什么类型的异常。
 通过在程序在CallBackPtr typedef中采用更严格的异常规格来解决问题:

typedef void (*CallBackPtr)(int eventXLocation,

 int eventYLocation,

 void *dataToPassBack) throw();

这样定义typedef后,如果注册一个可能会抛出异常的callback函数将是非法的:
// 一个没有异常规格的回调函数


void callBackFcn1(int eventXLocation, int eventYLocation,

 void *dataToPassBack);

void *callBackData;

...

CallBack c1(callBackFcn1, callBackData);

 //错误!callBackFcn1可能

 // 抛出异常

//带有异常规格的回调函数

void callBackFcn2(int eventXLocation,

 int eventYLocation,

 void *dataToPassBack) throw();

CallBack c2(callBackFcn2, callBackData);

 // 正确,callBackFcn2

 // 没有异常规格

 传递函数指针时进行这种异常规格的检查,是语言的较新的特性,所以有可能你的编译器不支持这个特性。如果它们不支持,那就依靠你自己来确保不能犯这种错误。  避免调用unexpected的第三个方法是处理系统本身抛出的异常。这些异常中最常见的是bad_alloc,当内存分配失败时它被operator new 和operator new[]抛出。如果你在函数里使用new操作符,你必须为函数可能遇到bad_alloc异常作好准备。

 现在常说预防胜于治疗(即:做任何事都要未雨绸缪 译者注),但是有时却是预防困难而治疗容易。也就是说有时直接处理unexpected异常比防止它们被抛出要简单。例如你正在编写一个软件,精确地使用了异常规格,但是你必须从没有使用异常规格的程序库中调用函数,要防止抛出unexpected异常是不现实的,因为这需要改变程序库中的代码。

 虽然防止抛出unexpected异常是不现实的,但是C++允许你用其它不同的异常类型替换unexpected异常,你能够利用这个特性。例如你希望所有的unexpected异常都被替换为UnexpectedException对象。你能这样编写代码:
class UnexpectedException {}; // 所有的unexpected异常对象被
 //替换为这种类型对象
void convertUnexpected() // 如果一个unexpected异常被
{ // 抛出,这个函数被调用
 throw UnexpectedException();

}

 通过用convertUnexpected函数替换缺省的unexpected函数,来使上述代码开始运行。:
set_unexpected(convertUnexpected);

 当你这么做了以后,一个unexpected异常将触发调用convertUnexpected函数。Unexpected异常被一种UnexpectedException新异常类型替换。如果被违反的异常规格包含UnexpectedException异常,那么异常传递将继续下去,好像异常规格总是得到满足。(如果异常规格没有包含UnexpectedException,terminate将被调用,就好像你没有替换unexpected一样)

 另一种把unexpected异常转变成知名类型的方法是替换unexpected函数,让其重新抛出当前异常,这样异常将被替换为bad_exception。你可以这样编写:
void convertUnexpected() // 如果一个unexpected异常被
{ //抛出,这个函数被调用

 throw; // 它只是重新抛出当前

} // 异常

set_unexpected(convertUnexpected);

 // 安装 convertUnexpected

 // 做为unexpected

 // 的替代品

 如果这么做,你应该在所有的异常规格里包含bad_exception(或它的基类,标准类exception)。你将不必再担心如果遇到unexpected异常会导致程序运行终止。任何不听话的异常都将被替换为bad_exception,这个异常代替原来的异常继续传递。

 到现在你应该理解异常规格能导致大量的麻烦。编译器仅仅能部分地检测它们的使用是否一致,在模板中使用它们会有问题,一不注意它们就很容易被违反,并且在缺省的情况下它们被违反时会导致程序终止运行。异常规格还有一个缺点就是它们能导致unexpected被触发,即使一个high-level调用者准备处理被抛出的异常,比如下面这个几乎一字不差地来自从前例子:

class Session { // for modeling online
public: // sessions
 ~Session();
...
private:
 static void logDestruction(Session *objAddr) throw();

};
Session::~Session()
{

 try {
 logDestruction(this);

 }
 catch (...) { }

}

 session的析构函数调用logDestruction记录有关session对象被释放的信息,它明确地要捕获从logDestruction抛出的所有异常。但是logDestruction的异常规格表示其不抛出任何异常。现在假设被logDestruction调用的函数抛出了一个异常,而logDestruction没有捕获。我们不会期望发生这样的事情,但正如我们所见,很容易就会写出违反异常规格的代码。当这个异常通过logDestruction传递出来,unexpected将被调用,缺省情况下将导致程序终止执行。这是一个正确的行为,但这是session析构函数的作者所希望的行为么?作者想处理所有可能的异常,所以好像不应该不给session析构函数里的catch块执行的机会就终止程序。如果logDestruction没有异常规格,这种事情就不会发生(一种防止的方法是如上所描述的那样替换unexpected)。

 以全面的角度去看待异常规格是非常重要的。它们提供了优秀的文档来说明一个函数抛出异常的种类,并且在违反它的情况下,会有可怕的结果,程序被立即终止,在缺省时它们会这么做。同时编译器只会部分地检测它们的一致性,所以他们很容易被不经意地违反。而且他们会阻止high-level异常处理器来处理unexpected异常,即使这些异常处理器知道如何去做。
 综上所述,异常规格是一个应被审慎使用的特性。在把它们加入到你的函数之前,应考虑它们所带来的行为是否就是你所希望的行为。

了解异常处理的系统开销
 为了在运行时处理异常,程序要记录大量的信息。无论执行到什么地方,程序都必须能够识别出如果在此处抛出异常的话,将要被释放哪一个对象;程序必须知道每一个入口点,以便从try块中退出;对于每一个try块,他们都必须跟踪与其相关的catch子句以及这些catch子句能够捕获的异常类型。这种信息的记录不是没有代价的。虽然确保程序满足异常规格不需要运行时的比较(runtime comparisons),而且当异常被抛出时也不用额外的开销来释放相关的对象和匹配正确的catch字句。但是异常处理确是有代价的,即使你没有使用try,throw或catch关键字,你同样得付出一些代价。

 让我们先从你不使用任何异常处理特性也要付出的代价谈起。你需要空间建立数据结构来跟踪对象是否被完全构造(constructed)(参见条款M10),你也需要CPU时间保持这些数据结构不断更新。这些开销一般不是很大,但是采用不支持异常的方法编译的程序一般比支持异常的程序运行速度更快所占空间也更小。

 在理论上,你不能对这些代价进行选择:异常是C++的一部分,C++编译器必须支持异常。也就是说,当你不用异常处理时你不能让编译器生产商消除这方面的开销,因为程序一般由多个独立生成的目标文件(object files)组成,只有一个目标文件不进行异常处理并不能代表其他目标文件不进行异常处理。而且即使组成可执行文件的目标文件都不进行异常处理,那么还有它们所连接的程序库呢?如果程序的任何一部分使用了异常,其它部分必须也支持异常。否则在运行时程序就不可能提供正确的异常处理。

 不过这只是理论,实际上大部分支持异常的编译器生产商都允许你自由控制是否在生成的代码里包含进支持异常的内容。如果你知道你程序的任何部分都不使用try,throw或catch,并且你也知道所连接的程序库也没有使用try,throw或catch,你就可以采用不支持异常处理的方法进行编译,这可以缩小程序的尺寸和提高速度,否则你就得为一个不需要的特性而付出代价。随着时间的推移,使用异处理的程序库开始变得普遍了,上面这种方法将逐渐不能使用,但是根据目前的软件开发情况来看,如果你已经决定不使用任何的异常特性,那么采用不支持异常的方法编译程序是一个性能优化的合理方法。同样这对于想避开异常的程序库来说也是一个性能优化的好方法,这能保证异常不会从客户端程序传递进程序库里,不过同时这样做也会妨碍客户端程序重定义程序库中声明的虚拟函数,并且不允许有在客户端定义的回调函数。

 使用异常处理的第二个开销来自于try块,无论何时使用它,也就是当你想能够捕获异常时,那你都得为此付出代价。不同的编译器实现try块的方法不同,所以编译器与编译器间的开销也不一样。粗略地估计,如果你使用try块,代码的尺寸将增加5%-10%并且运行速度也同比例减慢。这还是假设程序没有抛出异常,我这里讨论的只是在程序里使用try块的开销。为了减少开销,你应该避免使用无用的try块。

 编译器为异常规格生成的代码与它们为try块生成的代码一样多,所以一个异常规格一般花掉与try块一样多的系统开销。什么?你说你认为异常规格只是一个规格而已,你认为它们不会产生代码?那么好,现在你应该对此有新的认识了。

 现在我们来到了问题的核心部分,看看抛出异常的开销。事实上我们不用太关心这个问题,因为异常是很少见的,这种事件的发生往往被描述为exceptional(异常的,罕见的)。80-20规则告诉我们这样的事件不会对整个程序的性能造成太大的影响。但是我知道你仍旧好奇地想知道如果抛出一个异常到底会有多大的开销,答案是这可能会比较大。与一个正常的函数返回相比,通过抛出异常从函数里返回可能会慢三个数量级。这个开销很大。但是仅仅当你抛出异常时才会有这个开销,一般不会发生。但是如果你用异常表示一个比较普遍的状况,例如完成对数据结构的遍历或结束一个循环,那你必须重新予以考虑。

 不过请等一下,你问我是怎么知道这些事情的呢?如果说支持异常对于大多数编译器来说是一个较新的特性,如果说不同的编译器的异常方法也不同,那么我如何能说程序的尺寸将增大5%-10%,它的速度也同比例减慢,而且如果有大量的异常被抛出,程序运行速度会呈数量级的减慢呢?答案是令人惊恐的:一些传闻和一些基准测试(benchmarks)。事实是大部分人包括编译器生产商在异常处理方面几乎没有什么经验,所以尽管我们知道异常确实会带来开销,却很难预测出开销的准确数量。

 谨慎的做法是对本条款所叙述的开销有了解,但是不深究具体的数量(即定性不定量译者注)。不论异常处理的开销有多大我们都得坚持只有必须付出时才付出的原则。为了使你的异常开销最小化,只要可能就尽量采用不支持异常的方法编译程序,把使用try块和异常规格限制在你确实需要它们的地方,并且只有在确为异常的情况下(exceptional)才抛出异常。如果你在性能上仍旧有问题,总体评估一下你的软件以决定异常支持是否是一个起作用的因素。如果是,那就考虑选择其它的编译器,能在C++异常处理方面具有更高实现效率的编译器。