Questions:
(1)何为“异常安全性”?
(2)如何防止资源泄露?
(3)如何防止数据败坏?
(4)什么是异常安全函数?
Answers:
(1)何为“异常安全性”?
一个函数如果说是“异常安全”的,必须同时满足以下两个条件:
1.不泄漏任何资源;
例如以下程序:
例1:
int getKey(char* fileName)
{
FILE *fp;
int key;
fp = fopen(fileName, "r");
fcanf(fp, "%d", &key);
return key;
}
该程序缺少fclose(),会对打开的文件造成不确定的操作,很容易造成资源的泄露,其他资源(如信号量、数据库连接)也很容易出现类似的情况!该函数就不是一个“异常安全”的函数!
这是一个很明显的人为失误——没有添加fclose代码,还有一些隐形的危险也会带来“资源泄露”,这些危险通常是由不确定的exception带来的,比如:
例2:
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);
}
在class PrettyMenu的changeBackground这个成员函数中,对信号量mutex确实执行了lock和unlock操作,貌似不会造成资源泄露,然而,事实上却非如此!这是一个很糟糕的函数!
在bgImage = new Image(imgSrc)这条语句中,一旦new Image()导致异常(可能是bad_alloc异常),后面的unlock就不会执行,因此就可能造成资源泄露!
2.不允许数据败坏。
在例2这个程序中,如果“new Image(imgSrc)”抛出异常,bgImage就指向一个已被删除的对象,imageChanges也已被累加,而其实并没有新的图像被成功安装起来。
(2)如何防止资源泄露?
防止资源泄露可以通过建立一个单独的class来管理资源,使资源”在构造期间获得,在析构期间释放“。
对于例2的程序,我们可以这样修改之:
例3:
class Lock
{
public:
explicit Lock(Mutex *pm):mutexPtr(pm)
{
Lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
class PrettyMenu{
public:
...
void changeBackground(std::istream& imgSrc);
...
private:
Mutex mutex;
Image* bgImage;
int imageChanges;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
delete bgImage; //删除旧的背景图片
++imageChanges; //修改图像变更次数
bgImage = new Image(imgSrc); //安装新的背景图像
}
局部Lock变量m1在建立的时候会调用lock函数锁住mutex,函数执行完后会自动析构,在析构的同时会调用unlock,不管new Image()会不会出现异常,都会正常的unlock。
(3)如何防止数据败坏?
通过智能指针,我们可以实现简单的”防止数据败坏“。
例4:
class PrettyMenu{
...
std::tr1::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
这里不再需要手动delete旧图像,因为这个动作已经由智能指针bgImage内部处理掉了,此外,删除动作只发生在新图像被成功创建之后,因为delete动作只在reset函数内部被调用,如果从未进入那个函数,也就永远不会delete,这样就可以保证如果new Image()分配失败不会让bgImage指向一个被删除了的对象。
(4)什么是异常安全函数?
C++中’异常安全函数”提供了三种安全等级:
1. 基本承诺:如果异常被抛出,对象内的任何成员仍然能保持有效状态,没有数据的破坏及资源泄漏。但对象的现实状态是不可估计的,即不一定是调用前的状态,但至少保证符合对象正常的要求。
2. 强烈保证:如果异常被抛出,对象的状态保持不变。即如果调用成功,则完全成功;如果调用失败,则对象依然是调用前的状态。
3. 不抛异常保证:函数承诺不会抛出任何异常。一般内置类型的所有操作都有不抛异常的保证。
如果一个函数不能提供上述保证之一,则不具备异常安全性。
对于第一个安全等级”基本承诺“,通过上面几个例子知道,可以通过资源管理对象和智能指针来实现,而对于第二个安全等级,我们可以通过”copy and swap“的策略来实现!其原理很简单:即先对打算修改的对象做出一个副本(copy),在副本上做必要的修改。如果出现任何异常,原对象依然能保证不变。如果修改成功,则通过不抛出任何异常的swap函数将副本和原对象进行交换(swap)。
代码如下:
例5:
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));
pNew->bgImage.reset(new Image(imgSrc)); //修改副本
++pNew->imageChanges;
swap(pImpl, pNew);//置换数据
}
在changeBackground函数中,先将需要修改的数据pImgl制作一份副本放在pNew中,注意是数据副本,不是指针的副本,所以这里用的是std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); 而不是std::tr1::shared_ptr<PMImpl> pNew = pImpl,后者无法实现制作副本,以后所有的修改都是在副本上修改,修改成功之后才通过swap与原变量置换!
copy and swap策略虽然做出“全有或全无”改变的一个好办法,但一般而言并不保证整个函数有强烈的异常安全性。因为如果在函数中调用了其他的函数,而其他函数如果又修改了非成员变量(比如做数据库的操作),就很难保证对象的状态不变,无法恢复到初始状态! 例如:
void somefunc()
{
…
f1();
f2();
…
}
显然,如果f1或f2的异常安全性比“强烈保证”低,就很难让someFunc成为“强烈异常安全”。如果f1和f2都是“强烈异常安全”,情况并不因此好转。毕竟,如果f1圆满结束,程序状态在任何方面都有可能有所改变,因此如果f2随后抛出异常,程序状态和someFunc被调用前并不相同,甚至当f2没有改变任何东西时也是如此。
问题出现在“连带影响”,如果由函数只操作局部状态,便相对容易的提供强烈保证,但是函数对“非局部性数据”有连带影响时,提供强烈保证就困难的多。例如,如果调用f1带来的影响是某个数据库被改动了,那就很难让someFunc具备强烈安全性。另一个主题是效率。copy-and-swap得好用你可能无法(或不愿意)供应的时间和空间。所以,“强烈保证”并不是在任何时候都显得实际。
当“强烈保证”不切实际时,你就必须提供“基本保证”。
你应该挑选“现实可操作”条件下最强烈等级,只有当你的函数调用了传统代码,才别无选择的将它设为“无任何保证”。
Remember:
1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
2.“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
3.函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。