?
?
条款29:为“异常安全”而努力是值得的
看完这个条款,第一感觉就是之前写的代码多么具有风险。
先看一个书上的例子,假设有个class用来表现夹带背景图案的GUI菜单,这个class也要用于多线程环境当中,所以我们考虑用了一个互斥器(mutex)作为并发控制(concurrency control)之用:
?
class PrettyMenu
{
public:
...
void changeBackground(std::istream& imgSrc); //改变图片背景
...
private:
Mutex mutex; //互斥器
Image* bgImage; //目前的背景图像
int imageChangesCounts; //背景图像被改变的次数
};
// 下面是PrettyMenu的changeBackground函数的一个可能实现:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex); //取得互斥器
delete bgImage; //摆脱旧的背景图像
++imageChangesCounts; //修改变更次数
bgImage_ = new Image(imgSrc); //安装新的背景图像
unlock(&mutex); //释放互斥器
} 从“异常安全性”来看,这个函数很糟糕。“异常安全”有两个条件,而这个函数没有满足其中任意一个条件。
?
1)不泄露任何资源:一旦“new Image(imgSrc)”导致异常,对unlock的调用就绝对不会执行,于是互斥器就永远锁住了。
2)不允许数据破坏:如果“new Image(imgSrc)”抛出异常,bgImage就是指向了一个已被删除的对象,且imageChanges也被累加,而其实没有新的图像被成功安装。
解决资源泄露很简单,,我们可以用资源管理类来确保互斥器锁定后一定会释放。
异常安全函数提供以下三种保证之一:
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
强烈保证:如果异常被抛出,程序状态不改变。
不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。
一般而言我们会想提供最高保证,但是我们很难在C part of C++中调用一个完全没有可能抛出异常的函数。任何使用动态内存的东西一旦没有足够内存满足要求,都会抛出一个bad_alloc异常。
?
class PrettyMenu
{
...
std::tr1::shared_ptr
bgImage; }; void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock(&mutex); //获得互斥器并确保它稍后被释放 bgImage.reset(new Image(imgSrc)); ++imageChangesCounts; }上面是我们优化后的代码,但是该函数也只能提供“基本的异常安全保证”。因为如果Image构造函数抛出异常,有可能输入流的读取记号已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。
有个一般化的设计策略可以达到这个目的,这个策略被称为“copy and swap”,俗话说“名如其人”,这个原则其实就是:为你打算修改的对象保存一个副本,然后在该副本上修改。若修改过程中发生异常,问题不大,因为原对象状态没有被改变。修改动作完成以后进行“副本与原对象互换”操作。
?
?
struct PMImpl
{
std::tr1::shared_ptr
bgImage; int imageChangesCounts; }; class PrettyMenu { ... void changeBackground(std::istream& imgSrc) { using std::swap; Lock m1(&mutex); std::tr1::shared_ptr
pNewImpl(new PMImpl(*pImpl)); //make copy pNewImpl->bgImage_.reset(new Image(imgSrc));//handle copy ++pNew->imageChangesCounts; swap(pImpl,pNewImpl); //swap } private: Mutex mutex_; std::tr1::shared_ptr
pImpl; };
我们注意到copy-and-swap的关键在于“修改对象数据副本,然后在一个不抛异常的函数中将修改后的数据和原件置换”,因此必须为每一个即将被改动的对象做一个副本,那得耗用你
可能无法供应的时间和空间。
?
这是一个很实际的问题:我们都希望提供“强烈保证”;当它可被实现时你的确应该提供它,但“强烈保证”并非在任何时刻都显得实际。
当强烈保证不切实际的时候,你就必须提供基本保证。
在实际的开发当中你可以为某些函数提供强烈保证,但效率和复杂度带来的成本会使得你不得不去放弃它,万一实际不可行,使你退而求其次地只提供基本保证,任何人都不该因此责难你。对许多函数而言,“异常安全性之基本保证”是一个绝对通情达理的选择。
?
总结:
1)异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构被破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
2)“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
3)函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
?
条款30:透彻了解inlining的里里外外
inline函数,你可以调用它们又不蒙受函数调用招致的额外开销,编译器就会对函数本体执行语境相关最优化,而大部分编译器不会对着一个非inline函数调用动作执行如此之最优化。
由于对inline函数的每一个调用都以函数本体替换之,所以说这样就可能增加你的目标代码,在一台有限的机器上,过度热衷inlining会造成程序体积太大,即使拥有虚拟内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随而来的效率损失。
以上是inline函数的优缺点,所以我们依旧要辩证性的看待inline函数。
不能忽视的一点是,inline函数仅仅是一个申请,而不是强制命令,所以它是会可能被编译器驳回的。这个申请可以隐喻提出,也可以明确提出。
隐喻提出就是将函数定义于class定义式内,这样的函数通常是成员函数或者是friend函数。如下:
?
class Person
{
public:
...
int age() const
{
return theAge; // 一个隐喻的inline申请
}
...
private:
int theAge;
} 明确声明就是在函数之前加关键字inline。例如标准的max template往往这样实现出来:
?
?
template
inline const T& std::max(const T &a, const T &b)
{
return a < b ? b : a;
}
?
inline函数通常一定要被置于头文件内,其在大多数C+