读书笔记中涉及到的所有代码实例可通过https://github.com/LuanZheng/EffectiveCPlusPlus.git进行下载获得。
Item29 为“异常安全”而努力是值得的
“异常安全”有两个条件
1)不泄露任何资源。
2)不允许数据败坏。
例如下列代码:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc); //注意,若new抛出异常,则unlock无法得到执行,资源泄露 //imangeChanges无法得到恢复,数据败坏
unlock(&mutex);
}
为了避免资源泄露,我们需要适用资源管理类来管理资源。
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc); //注意,这时候,不需要主动调用unlock了,集中精力对付数据败坏
}
异常安全函数提供下面三个保证:
1)基本承诺,如果异常被抛出,程序内的任何事物仍然保持在有效状态之下。
2)强烈保证,如果异常被抛出,程序状态不改变。程序回回复到调用函数之前的状态。
3)不抛掷保证,承诺绝不抛出异常,因为它们重视能够完成它们原先承诺的功能。
继续努力,我们尝试为changeBackground函数提供强烈保证。
class PrettyMenu
{
...
std::tr1::shared_ptr bgImage;
...
}
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
//注意,这时候,如果new抛出异常,则reset函数不会执行,而delete只在reset里面被调用,
//因此异常不会破坏原数据
bgImage.reset(new Image(imgSrc));
++imageChanges; //如果new抛出异常,则imageChanges不变
}
知识补充,tr1::shared_ptr::reset方法
// shared_ptr::reset example
#include #include
int main ()
{
std::shared_ptr<int> sp; // empty
sp.reset (new int); // takes ownership of pointer
*sp=10;
std::cout << *sp << '\n';
sp.reset (new int); // deletes managed object, acquires new pointer
*sp=20;
std::cout << *sp << '\n';
sp.reset(); // deletes managed object
return 0;
}
使用copy-and-swap获取强烈保证
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
Lock m1(&mutex);
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //这里制作了一个副本pNew
pNew->bgImage.reset(new Image(imgSrc)); //改变的均为副本
++pNew->imageChanges; //改变的均为副本
swap(pImpl, pNew); //置换数据,changeBackground退出时释放mutex
}
但看下面的例子,
void someFunc()
{
...
f1();
f2();
...
}
即使someFunc使用的copy and swap技术,但如果f1或f2的运行过程中仍可能导致someFunc没有达到强烈保证的要求。
另一个主题是这样实现强烈保证,会损失效率。
当强烈保证不切实际时,你就必须提供“基本保证”。
Item30 透彻了解inline的里里外外
优点:
调用inline函数而又不需要蒙受函数调用所导致的额外开销。
缺点:
会造成程序体积变大,随之伴随降低高速缓存的命中率,从而降低效率。
换个角度说,如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产生的码更小。inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。
隐式内联函数,这样的函数通常是成员函数+友元函数,在函数声明(头文件)的同时进行定义与实现,显式内联函数的做法是在函数声明时加上inline关键字。
注意区分内联函数与模板函数,虽然两者都常常在头文件中实现,但模板函数是为了让编译器知道函数调用时如何具现话函数而定义在头文件中。
inline函数(即内联函数)对编译器而言必须是可见的,以便能够在调用点展开该函数,与非inline函数不同的是,inline函数必须在调用该函数的每个文件中定义。当然,对于同一程序的不同文件,如果inline函数出现的话,其定义必须相同。
正因为如此,建议把inline函数的定义放到头文件中,在每个调用该inline函数的文件中包含该头文件。这种方法保证了每个inline函数只有一个定义,且程序员无需复制代码,并且不可能在程序的生命周期中引起无意的不匹配的事情。
——摘自《C++ Primer》(第三版)
构造函数和析构函数不要用作inline.
Item31 将文件的编译依存关系降至最低
降低文件的编译依存关系,需要减少include文件的数目,考虑前置声明,但前置声明存在的问题
1)若标准库中提供的类型,不能使用前置声明。
2)编译器必须在编译期间知道对象的大小(主要是这一条比较重要)。
Handle class设计
pimpl idion设计,main class只内含一个指针成员,指向其实现类。这种classes内的指针名称往往就是pImpl. 通过这种设计使得接口与实现分离。
这个分离的关键在于以声明的依存性替换定义的依存性,那正式编译依存性最小化的本质;现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式相依。
1)如果使用object references或object pointers可以完成任务,就不要使用objects.你可以只靠一个类型声明式就定义出指向该类型的references和pointers;但如果定义某些类型的objects,就需要用到该类型的定义式。
2)如果能够,尽量以class声明式替换class定义式。如果能够将“提供class定义式”(通过#include完成)的义务从“函数声明所在”只头文件转移到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性去掉。
3)为声明式和定义式提供不同的头文件。一个用于声明式,仅包括前置声明。另一个用于定义式,普通头文件。
Interface class设计
另一个方法是使用抽象基类。基类提供工厂方法,用来创建出不同的派生类对象。利用实际的对象来完成具体的工作。
Handle class和Interface class解除了接口和实现的耦合,从而降低文件之间的编译依存性。
例子见Item31