C++ Exceptional 写异常安全的代码

来源:互联网 发布:linux移植到arm教程 编辑:程序博客网 时间:2024/05/10 21:10

C++ Exceptional中有一大段的章节是写异常安全的,这可能也是很多程序员在平时的编程中所容易忽视的一个部分,这里我大体总结一下这本书中所涉及到的有关异常安全的知识。

异常安全有三种层次:
1. 基本的异常安全: 当异常发生时,保证没有内存泄露
2. 强的异常安全:当异常发生时,不但要保证没有内存泄露,还要保证程序的状态和异常发生前是一致的。
3. 异常永不发生的保证: 即保证在任何情况下,异常都不会发生。

在写代码时,尽量保证两个原则:
1. 异常安全,实现以上三种异常安全层次之一。
2. 异常中立(exception neutral),当有异常发生时,捕获后要重新抛出,让调用者知道。

我们讨论一下C++中new操作符和delete操作符的异常安全问题。
new操作符本身是异常安全的,为什么这样说呢,因为如果在new一个或多个对象的时候,一般会发生两种类型的错误。第一种是bad_alloc异常,即没有申请到内存,因为这个时候连内存都没申请到,所以发生了异常,也不会有内存泄露。另外一种情况是,对象的构造发生了异常,这个时候此对象会构造失败,并且已经构造好的对象也会被析构掉,所占用的内存也会被释放掉。所以说new操作符是异常安全的。
对已delete操作符来说,情况就要复杂一些了。在delete一个数组对象时,若其中一个对象发生了异常,将导致在这个对象之后的对象无法被正确析构,从而发生内存泄露。所以说这个就要求我们所提供的类的析构函数是不能抛出异常的。

把有可能产生异常的代码移到另一个函数中。只有当另一个函数里的事情成功做完后,你才可以用不可能抛出异常的操作改变程序的状态
代码示例如下:

template<class T>class Stack{public:    Stack();    ~Stack();    Stack(const Stack&);    Stack& operator=(const Stack&);    /*...*/private:    T* v_;           //ptr to  a memery area big    size_t vsize_;   //enough for 'vsize_' T's    size_t vused_;   //of T's acctually in use}template<class T>T* NewCopy( const T* src,            size_t srcsize,            size_t destsize){    assert(destsize >= srcsize);    T* dest = new T[destsize];    try    {        copy(src, src+srcsize, dest);    }    catch(...)    {        delete [] dest; // this can't throw        throw;          // rethrow original exception,                         // exeption neutral pinciple    }    return dest;}//copy constructiontemplate<class T>Stack<T>::Stack(const Stack<T>& other):v_(NewCopy(other.v_,            other.vsize_,            other.vsize_)), vsize_(other.vsize), vused_(other.vused) { }//copy assignmenttemplate<class T>Stack<T>& Stack<T>::operator=(const Stack<T>& other){    if(this != &other)    {        T* v_new = NewCopy(other.v_,                           other.vsize_,                           other.vsize_);        delete [] v_; //this can't throw        v_ = v_new;        vsize_ = other.vsize_;        vused_ = other.vused_;    }    return *this; // safe, no copy involved}

从以上代码可以看出,拷贝构造函数和赋值构造函数都涉及到了对象的构造和复制,所以是有可能产生异常,但可以把这些产生异常的代码包装到另外一个函数中去,让所有的异常都在另外一个函数中。这样,当此函数成功执行后,就不用担心异常所产生的后果了。这里体现的是一种异常封装的思想。

接下来再看一下Stack中Push和Pop的实现
首先是Pop的实现,如下:

template<class T>void Stack<T>::push(const T& t){    if(vused_ == vsize_) //grow if necessary                         // by some grow factor    {        size_t vsize_new = vsize_ * 2 + 1;        T* v_new = NewCopy(v_, vsize_, vsize_new);        delete [] v_; //this can't throw        v_ = v_new;        vsize_ = vsize_new;    }    v_[vused_] = t;    ++vused;}

以上Pop的实现是异常安全的,原因如下;
1. if语句中有可能发生异常的代码在NewCopy中,所以哪怕NewCopy中发生了异常,也不会影响此类的状态,更不会引起此类的内存泄露。
2. v_[vused_] = t 是有可能发生异常的,但这句如果发生了异常,++vused_ 是不会被执行到的,所以类的内部状态保持了一致。

一下是Pop的实现代码:

template<class T>T Stack<T>::Pop(){    if(vused_ == 0)    {        throw "Pop from empty stack";    }    else    {        T result = v_[used - 1];        --vused_;        return result;    }}

让我们来分析下上面的代码,如果vused == 0,这个时候此函数当然是异常安全的。第二个发生异常的地方 T result = v_[vused_ -1],这里发生异常也可以保证此函数是异常安全的,因为类的内部状态还没有改变,所以是异常安全的。但这里有个不容易被发现的地方,就是return result 也有可能发生异常,因为这里也涉及到了一个临时对象的创建。而这个时候如果发生了异常,就会出问题了,首先外部的代码没有得到返回的值,但此类的内部状态却已经改变了。要解决这个问题,代码修改如下:

template <class T>void Stack<T>::Pop(T& result){    if(vused_ == 0)    {        throw "pop from empty stack";    }    else    {        result = v_[vused_ - 1];        --vused;    }}

以上利用引用的方式保证了,当外部调用确实获得值后,类的内部状态才会变化。而如果在result = v_[vused_ - 1] 处发生异常时,外部调用不会得到此值,但类的状态也没有改变,所以也是异常安全的。

最后,再深入想一下上面的这个问题,为什么Pop在有可能发生异常时,会导致不是异常安全的呢。这是因为这个函数承担了两个责任,一个是返回栈顶上的值,另外一个则是减掉一个计数,而也正是这种职责的交叉导致了异常的不安全。下面是STL中将这两者分开实现后的结果:

template<class T>T& Stack<T>::Top(){    if(vused_ == 0)    {        throw "empty stack";    }    return v_[vused_ - 1];}template<class T>void Stack<T>::Pop(){    if(vused_ == 0)    {        throw "Pop from empty stack";    }    else    {        --vused_;    }}

上面这种方式是利用把职能分开的方式来避免类型不安全的。

利用把内存的申请、销毁和对象的创建分开的方式来避免内存泄露
利用这种方式,甚至可以不用一个try-catch来完成异常安全。看下面代码:

template<class T>class StackImpl{public:    StackImpl(size_t size = 0);    ~StackImpl();    void Swap(StackImpl& other) throw();    T*     v_;     // ptr to a memery area big    size_t vsize_; // enough for 'vsize_' T's    size_t vused_; // of T's actually in useprivate:    // private and undefined: no copying allowed    StackImpl( const StackImpl& );    StackImpl& operator=( const StackImpl& );}// constructortemplate<class T>StackImpl<T>::StackImpl( size_t size ):v_( static_cast<T*>     ( size == 0? 0        : operator new(sizeof(T)*size)), vsize_(size), vused_(0) { }// destructortemplate <class T>StackImpl<T>::~StackImpl(){    destroy(v_, v_ + vused_); // this can't throw    operator delete (v_);}// Some standard helper functions// construct() constructs a new object in // a given location using an initial value//template <class T1, class T2>void construct(T1 *p, const T2& value){    new (p)T1(value);}// destroy() destroys an object or a range// of objects//template <class T>void destroy(T* p){    p->~T();}template <class FwdIter>void Destroy(FwdIter first, FwdIter last){    while( first != last)    {    destroy(&*first);    ++first;    }}// swap() just exchanges two valuesvoid swap(T& a, T& b){    T temp(a);    a = b;    b = temp;}template <class T>void StackImpl<T>::Swap(StackImpl& other) throw(){    swap(v_,     other.v_);    swap(vsize_, other.vsize_);    swap(vused_, other.vused_);}template <class T>class Stack{public:L    Stack(size_t size = 0)    :imple_(size)    {    }    ~Stack(){}    Stack(const Stack& other)    :impl_(other.impl_.vused)    {        while( impl_.vused_ < other.impl_.vused_)        {            construct(impl_.v_ + impl_.vused_,                      other.impl_.v_[impl_.vused_]);            ++impl_.vused;        }    }    Stack& operator=(const Stack&)    {        Stack temp(other);        impl_.Swap(temp.impl_); // this can't throw        return *this;    }    size_t Count() const    {        return impl.vused_;    }    void Push(const T&)    {        if(impl_.vused_ == impl_.vsize_)        {            Stack temp(impl_.vsize_*2 + 1);            while(temp.Count < impl_.vused_)            {                temp.Push(impl_.v_[temp.Count()]);            }            temp.Push(t);            impl_.Swap(temp.impl_);        }        else        {            construct(impl_.v_ + impl_.vused_, t);            ++impl_.vused;        }    }    T& Top() // if empty, throws exception    {        if(impl_.vused_ == 0)        {            throw "empty stack";        }        return impl_.v_[impl_.vused_ - 1];    }          void Pop() // if empty, throws exception    {        if(impl_.vused_ == 0)        {            throw "pop from empty stack";        }        else        {            --impl_vused_;            destroy(impl_.v_ + impl_.vused_);;        }    }    private:    StackImpl<t> impl_; //private implementation};

以上实现代码较长,但里面无非下面几个关键点:
1. 如何在不利用try-catch的情况下,做到异常安全。这里用到的是把资源的申请和管理交给一个implement类,主类只负责对象的创建。这里用到了placement new和placement delete。
2. 这里又用到了Temp-Swap的方式来方便的在赋值的同时保证异常安全。

0 0
原创粉丝点击