在前三篇中我说明了有效创建一个类的前4个考虑步骤,现在就差最后一步了,考虑创建与类定义有关的异常类。 异常的概述 用户调用某个函数,函数可以在运行时检测到错误,但是不知道如何处理;用户呢,实际上知道在遇到这种错误时,该如何处理;为了解决这类问题,提出了异常的概念。异常的基本思想是:当函数检测到自己无法处理的错误时抛出一个异常,以便调用者(用户)能够处理这个异常。用户如果希望处理这种异常可以使用catch捕获这个异常。 传统的错误处理方式 (1)终止程序 (2)返回一个表示“错误”的值 (3)返回一个合法值,让程序处于某种非法状态 (4)调用一个预先准备好,在出现“错误”的情况下用的函数 (1)的方式,在未捕获异常的情况,默认发生的事情,换句话说,跟没有异常一样。但是我们可以做的更好不是吗?(2)调用者需要检查错误值,这容易使程序的体积加大,而且能够正常返回错误值是前提,但有时并不如人所愿。(3)事实上,即使能返回一个合法值,但是调用者往往很难注意到程序已经处在非法状态中了。(4)看上去与异常处理相似,但是是不是出现这种错误时,就一定要执行这个预先准备好的函数呢? 一个异常就是某个用于表示异常发生类的一个对象。检查到一个错误的代码段throw一个对象。一个catch语句表明它要处理某个异常。一个throw的作用就表示堆栈的一系列回退,直到找到适当的catch。 异常与资源管理 对于一个资源管理类来说,一旦在获取资源的过程中,或者使用资源的过程中,捕获到异常,需要释放掉自身占有的异常。当然可以使用try, catch语句,但是C++提出的“资源申请即初始化”的方式更优雅,更安全。 异常与new 一旦在使用new时,捕获到异常,那么处理异常的代码中必须调用对应的delete。 异常与资源耗尽 当堆内存由于申请的空间过大,已经耗尽资源时;C++默认调用_new_handler函数指针。
C++代码 void customized_new_handler(){ ... }; set_new_handler(&customize_new_handler); //std func void f() { void (*oldnh)() = set_new_handler(&customized_new_handler); try { // ... } catch(bad_alloc) { // ... } catch( ... ){ set_new_handler(oldnh); //reset handler throw; //re-throw } set_new_handler(oldnh); //reset handler } 上述是资源耗尽的一般情况,极端情况可能连new一个异常对象的空间也没有了,那怎么办?不用担心,C++语言已经帮我们想好了,那就是每一个C++程序实现都要求保留足够的存储,在资源耗尽的情况,仍然可以抛出bad_alloc。 异常与构造函数 因为构造函数其特殊性,无法返回一个独立的值供调用程序检查。异常处理机制允许从构造函数内部传出来错误信息。通常构造函数多与资源申请有关,所以建议采用“资源申请即初始化”的方式处理异常。 异常与成员初始化 将成员初始式包含在try,catch块内。 C++代码 class X { Vector v; // public: X(int); // }; X::X(int s) try :v(s) { // ... } catch(Vector::Size){ // ... } 异常与析构函数 析构函数的调用存在两种情况 I. 正常销毁对象,调用 II. 因异常,在捕获异常的处理块中调用; 对于后一种情况,绝不能让析构函数里抛出异常。如果真是这样,那就是异常处理机制的一次失败,并调用std::terminate()。那么抛出异常退出析构函数也违背了标准库的要求。 如果析构函数必须要调用一个可能抛出的异常的函数,可以通过try, catch块保护自己;当然保护方式要么吞掉所有可能抛出的异常,要么终止程序。如果要求析构函数调用的这个可以抛出异常的函数必须做出运行时异常反应的话,那么请考虑使用一个普通函数。(参考《Effective C++》条款08) 异常的描述 C++代码 void f() throw( x2, x3); //may throw only x2, x3 exceptions void f() //can throw any exception void f() throw(); //no exception thrown 异常的映射 unexpected()的行为与std::bad_exception映射; 与此set_new_handler()类似,对unexpected()的响应由_unexpected_handler决定,它又是通过<exception>中的std::set_unexpected()设置的。 用户自定义的异常映射 C++代码 void g() throw(Yerr); g()被在网络分布式环境下被调用,由于g()对网络异常一无所知,自然调用unexpected()。如果不希望g()调用unexpected(),那么需要g()处理所有网络情况可能抛出的那些网络异常,那么就需要重写g()。如果g()由于种种限制不能重写,我们只能通过重新定义unexpected()完成。 一、首先采用“资源申请即初始化”方式为unexpected()函数定义一个类 C++代码 //摘自《The C++ Programming Language》第14.6.3节 typedef void(*unexpected_handler)(); unexpected_handler set_unexpected(unexpected_handler); class STC { //store and reset class unexpected_handler old; public: STC(unexpected_handler f){ old = set_unexpected(f);} ~STC(){ set_unexpected(old);} }; 二、定义一个函数,使它具有我们希望的unexpected()的意义 C++代码 class Yunexpected:public Yerr{}; void throwY() throw (Yunexpected) { throw Yunexpected(); } 三、提供一个网络版g函数 C++代码 void networked_g() throw (Yerr) { STC xx(&throwY); g(); } 但是上述对于用户而言,只是知道因为调用g()产生一个unexpected异常,具体是什么网络异常并不知道,那么怎么办呢? 这时修改下Yunexpected的类定义和throwY()的定义,使之可以保存真正的异常信息即可。 C++代码 class Yunexpected:public Yerr{ public: Network_exception * pne; Yunexpected(Network_exception *p) : pne(p?p->clone():0){} ~Yunexpected(){delete pne;} }; void throwY() throw(Yunexpected){ try{ throw; //re-throw } catch(Network_exception& p){ throw Yunexpected(&p); } catch( ... ){ throw Yunexpected(0); } } 未捕获异常 缺省情况下,如果抛出一个异常未被捕获,那就会调用函数std::terminate()。 uncaught_exception由_uncaught_handler决定,uncaught_handler由std::set_terminate()设置。 标准异常体系 标准异常体系的样子如下图所示 其中exception在文件<exception>里给出 C++代码 class exception{ public: exception() throw (); exception(const exception&) throw(); exception& operator=(const exception&) throw(); virtual ~exception() throw(); virtual const char* what() const throw(); private: // ... }; 所有标准异常都由exception派生,然后不是所有的异常都由exception派生,所以通过捕捉exception想捕获所有异常是错误的想法。 如何定义一个完善的异常类,并且继承于std::exception体系呢? C++代码 #include <string> #include <exception> class MyBaseException : public std::exception { public: //Constructor without inner exception xxBaseException(const std::string& what = std::string("xxBaseException")) : xx_BaseException(0), xx_What(what) {} //Constructor with inner exception xxBaseException(const xxBaseException& innerException, const std::string& what = std::string("xxBaseException")) : xx_BaseException(innerException.clone()), xx_What(what) {} template <class T> // valid for all subclasses of std::exception xxBaseException(const T& innerException, const std::string& what = std::string("xxBaseException")) : xx_BaseException(new T(innerException)), xx_What(what) {} virtual ~xxBaseException() throw() { if(xx_BaseException) { delete xx_BaseException; } } //don't forget to free the copy of the inner exception const std::exception* base_exception() { return xx_BaseException; } virtual const char* what() const throw() { return xx_What.c_str(); } //add formated output for your inner exception here private: const std::exception* xx_BaseException; const std::string xx_What; virtual const std::exception* clone() const { return new xxBaseException(); } // do what ever is necesary to copy yourselve }; 上述的自定义xxBaseException还是比较简单的,还可以自己添加文件名,行号等信息,stackTrace深度等信息。 虽然在C++里提供了这样完备的异常处理机制,(似乎每种语言的异常处理机制大致相同,比如JAVA,C#),但是想象一下如果一个函数或者一个类中布满了这样的try, catch块,总归显得代码十分丑陋与笨拙。丑陋的同时也大大降低了开发人员对代码稳定性以及执行效率的自信。 那么怎么才能在避免写很多try,catch块的前提下,又能写出异常安全类或者方法呢? C++的实现者,Bjarne Stroustrup,和《Effective C++》的作者,Scott Meyers给了我们关于如何写异常安全类的建议: 实现”异常安全“类 说到异常安全类,那么其定义是必须要说的。(定义引自 "Exception Safty:Concepts and Techniques", Bjarne Stroustrup, Advances in Exception Handling Techniques, Lecture Notes in Computer Science 2022. Springer-Verlag, 2001, 60--76, Springer-Verlag) 引用 An operation on an object is said to be exeception safe if that operation leaves the object in a valid state when the operation is terminated by throwing an exception. 这段定义中需要注意这么几个关键字: 1.valid state:合法状态,合法状态可以是一个需要清除工作的错误状态,但这个错误状态一定是被良好定义的。这里良好定义意味着对象拥有合理的错误处理代码。 2.object:为了合理地定义合法状态,对象应该拥有一个invariant,一旦它的构造函数们确立了这个invariant,接下来针对这个对象的所有操作都会维持这个invariant,直到析构函数完成最后的清除工作。这段话里涉及到了一个invariant关键字,在维基百科里关于invariant的定义给出了两类,一类称为class invariant, 另一类是object invariant。这两个概念对于我们更好地理解如何“合理”定义“合法”状态有十分大的帮助。 3.throwing an exception: 操作是因为有异常抛出而终止的,object state也是针对这种情境下而言的。 class invariant 和 object invariant的简单介绍 这里我给出原文的原因是至今我没有找到非常贴切的术语来描述class invariant,虽然我可能理解了这个定义。 class invariant: A class invariant is an invariant used to constrain objects of a class. Methods of the class should preserve the invariant. The class invariant constrains the state stored in the object. (类不变量:类的约束条件,约束了存储在对象内的状态。) Class invariants are established during construction and constantly maintained between calls to public methods. (狭隘理解:这个类约束条件主要针对类管理的资源,无论是内存还是文件句柄或者数据库连接等等,只要资源在构造函数中被确立,那么后续的任何类对象操作,这个约束条件始终有效且保持不变。例如一个vector,类约束条件是在堆中申请下来) object invariant: is a programming construct consisting of a set of invariant properties that remain uncompromised regardless of the state of the object. This ensures that the object will always meet predefined conditions, and that methods may, therefore, always reference the object without the risk of making inaccurate presumptions. (对象不变量:是由一组无论对象状态如何都不妥协地保持不变的属性组成的编程概念。这确保了对象一直满足预定义的条件) 基于此,合法状态的定义对于异常安全类显得至关重要了;所以“合理状态”给出了三个保证 1. 基本保证:类不变量基本被保持,至少类在构造期确定下来的资源(内存,数据库连接,SOCKET,文件句柄等)不会有泄漏。 2. 强烈保证:类似于数据库的事务概念。针对类的关键操作,在基本保证的前提下要么操作完全成功,要么完全失败,无中间状态可言。 3. 不抛掷保证:在基本保证的前提下,对一些操作保证不抛掷异常。 那么实现异常安全类,基于不同的保证有一些编程技巧如下: (1)try块 (2)资源申请即初始化 其中资源申请即初始化这个点子的关键之处就是使资源的拥有者赋予一个局域化的对象。那么,在大多数情况,通过这个局域化对象的构造函数构建这个资源,当这个局域化对象被销毁时,其管理的资源自然地通过析构函数销毁掉,无论析构函数的调用时正常调用还是因为抛出异常调用。这样就保证了资源不会泄漏。 C++代码 //摘自《Effective C++》条款29 class PrettyMenu { public: ... void changeBackground(std::istream& imgSrc); ... private: Mutex mutex; Image* bgImage; int imageChanges; }; void PrettyMenu::changeBackground(std:;istream& imgSrc){ lock(&mutex); delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); unlock(&mutex); } 上述的代码显然有很多问题: 【1】资源泄漏:如果new Image(imgSrc)因为imgSrc给的源不正确而抛出异常,那么unlock语句不会被执行,于是锁永远不会被释放,造成资源泄漏; 【2】数据损坏:如果new Image(imgSrc)失败,抛出异常,那么bgImage就会指向一个已经被delete掉的对象,imageChanges也会被累加,而我们知道这违背事实。 所以上述代码可以这样改变下以解决上面两个问题 C++代码 class PrettyMenu { ... std::tr1::shared_ptr<Image> bgImage; ... }; void PrettyMenu::changeBackground(std::istream& imgSrc){ Lock ml(&mutex); bgImage.reset(new Image(imgSrc)); ++imageChanges; } 上述代码中,mutex资源的拥有者赋予给了一个局域化Lock对象ml。这样通过Lock类的构造函数确立了资源的获取,即获得锁;如果函数成功执行完后,函数内局域变量ml被自动销毁。销毁时,通过Lock类的析构函数自动释放锁;即使抛出异常时,由于销毁函数,所以锁同样也会被释放。 std::tr1::shared_ptr<T>是一个智能指针,通过智能指针的reset函数,不再需要手动删除旧bgImage对象,因为智能指针内部已经处理掉了,并且处理就图像的动作永远依据于new Image(imgSrc)语句的结果。 上述的代码已经缩短了函数changeBackground的长度,看起来更精练些。似乎也提供了强烈保证;但是美中不足的是imgSrc这个参数。如果Image构造函数抛出异常,istream的读取记号已被移走,而这样就跟该函数执行前有不同的地方了。所以上述changBackground实际只给出了基本保证。那么怎么改,才能给出强烈保证呢?考虑可以采用有效创建一个类(三)中的copy and swap方式。这种方式的基本思想就是为打算修改的对象创建一个副本,然后在副本上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改动都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。 实现上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓实现对象(即副本,因为副本完成修改动作)。这种手法通常称pointer to implementation, pimpl idiom。 所以就有了如下的可以提供强烈保证的修正代码 C++代码 struct PMImpl { std::tr1::shared_ptr<Image> bgImage; int imageChanges; }; class PrettyMenu { ... private: Mutex mutex; std::tr1::shared_ptr<PMImpl> pImpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc){ using std::swap; Lock ml(&mutex); std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //create copy pNew->bgImage.reset(new Image(imgSrc)); //modify copy ++pNew->imageChanges; swap(pImpl, pNew); //swap }