用PIMPL手法来保证异常安全
异常,在C++中既是一个好东西,又不是一个好东西。
因为它会打破程序的正常流程,虽然通过异常处理机制可以在一定程度上解决问题,但仍然很容易造成资源泄漏。
同时,用好了它,也会给你带来很多好处。
优秀的程序中,你不用再看到大块的try和catch。已经不需要了。
使用智能指针,RAII,以对象管理资源,可以保证对象析构时候资源的正常释放。没必要通过catch来捕获这个异常再释放。
如上所言其实是一种异常安全保证。
异常安全保证有三种级别,分别是Basic,Strong和No-throw。
上述的要看如何处理这个异常。如果仅仅是释放资源,但这样可能会增大结果的不确定性,这属于Basic保证级别。
如果能够正确处理,使结果能够有效恢复到出错前的状态,这种属于Strong保证级别。
这里着重要说的是No-throw,即不抛出任何异常的保证。
一般的做法是在函数头的最后加上throw()表示什么也不抛出。
它这样写并不意味着“绝对不会抛出任何异常”,而是意味着一旦抛出,就会调用unexpected指针所指的函数,默认就是terminate。
因此,作为程序员的我们,有义务去遵守函数异常规格(no-throw)的约定。
呃……先看看以前有人提出的如下问题:
class Widget { // ... private: T1 t1_; T2 t2_; }; |
Assume that any T1 or T2 operation might throw. Without changing the structure of the class, is it possible to write a strongly exception-safe Widget::operator=( const Widget& )?
翻译: |
假定T1或者T2的操作可能会抛出异常。那么,在不改变类结构的情况下,是否有可能写出一个重载的=运算符,使它具有强异常安全性? |
这个问题初看觉得不好解决。
因为我们按照常规思维去写operator=( const Widget& ) ……就是要让自己的成员t1_和t2_分别等于参数对象的t1_和t2_。可是这样因为T1或者T2的赋值不安全,必然会影响到Widget的赋值不安全。显然不是具有异常安全性的代码!
呃……其实呢,我们可以换角度去解决这个问题。
PIMPL的手法就应运而生。
PIMPL的全称是Pointer to Implementation。意思为这个类持有一个“指针”,它指向真正的成员数据。
这样,我们需要做的就是赋值操作符交换两个类的指针。
真正的内容被封装在另外一个地方。这样比喻吧——
我们可以认为真正的内容在保险箱中,但是保险箱里面是“危险品”,对它操作就意味着危险(抛出异常)。那么,赋值,有个比较好的方案就是让两个保险箱的持有者交换下钥匙。
呃……等等,有个问题——
赋值(a=b)的意思是让a的内容跟b一样,并不是交换a和b的内容。
没关系,交换是个常用手法。我们先拷贝一份b到一个临时对象c,然后交换a和c。这样就没问题了。
基于此,我们可以改进上述的Widget为如下的代码:
【以下代码参考《More Exceptional C++》 】
class Widget { public: Widget(); //初始化嵌套的WidgetImpl子对象,并让pimpl指向它 ~Widget(); //必要的析构 void Swap(Widget& other ); Widget& operator =(const Widget& other); // ... private: class WidgetImpl; //只是一个保险箱,内部的东西并不一定是this所指对象的。 auto_ptr<WidgetImpl> pimpl; //这个是钥匙,所指“保险箱”里才是真正内容。 };
class Widget::WidgetImpl { public: // ... T1 t1_; T2 t2_; }; |
这里,我们使用了智能指针auto_ptr和嵌套类。
……
那么,我们需要交换的就是auto_ptr指针,它是具有绝对异常安全性的(即no-throw保证)。
可以得到如下的swap:
void Widget::Swap( Widget& other ) throw() { auto_ptr<WidgetImpl> temp( pimpl); pimpl = other.pimpl; other.pimpl = temp; } |
它的每一步都是绝对安全的。
赋值操作符的重载代码如下:
Widget& Widget::operator=( const Widget& other ) { Widget temp( other ); Swap( temp ); return *this; } |
这样,我们就解决了文章最初提出的问题。
呃……swap函数,其实是std命名空间的一个标准函数。
我们既然这里实现了Widget类的特有Swap,按照《Effective C++》书中作者的思想,我们应该将它添加到标准std namespace中swap的一个完全特化(不是重载)。
因为这样做,对于使用这些代码的人,只要知道swap,就可以使用,而无需考虑这个swap的参数到底是否是一般特化的类型。
就算是不小心写出了std::swap的代码,也不会影响程序结果。具体实现如下:
namespace std{ template<> void swap<Widget>(Widget& a, Widget& b){ a.Swap(b); //注意上面的Swap首字母大写哦! } } |
关于PIMPL手法的更多细节,我个人十分推荐Herb Sutter的《Exceptional C++》一书。
虽然Scott Meyers 的《Effective C++》也有不少内容是讲解“异常安全”的,不过详细程度比不过《Exceptional C++》。
谢谢大家的关注!