首页 > 代码库 > 《Effective C++》学习笔记(八)
《Effective C++》学习笔记(八)
原创文章,转载请注明出处:http://blog.csdn.net/sfh366958228/article/details/38962661
条款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<Image> 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<Image> bgImage; int imageChangesCounts; }; class PrettyMenu { ... void changeBackground(std::istream& imgSrc) { using std::swap; Lock m1(&mutex); std::tr1::shared_ptr<PMImpl> 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<PMImpl> 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<typename T> inline const T& std::max(const T &a, const T &b) { return a < b ? b : a; }
inline函数通常一定要被置于头文件内,其在大多数C++程序中是编译期行为。
大部分编译器拒绝将太过复杂(含有循环、递归等)的函数inlining,而所有的virtual函数都不能inlining,因为virtual意味着“等待,知道运行期才确定调用哪个函数”,而inline意味“执行前先将动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,你就很难责备它们拒绝将函数本体inlining。
有时候编译器inline某个函数的同时,还可能为其生成一个函数本体(比如程序要取某个line函数地址),值得一提的是,编译器通常不对“通过函数指针而进行的调用”实施inling,这就是说line函数的调用可能是被inlined,也可能不被inlined,取决于调用的实施方式。
“将构造函数和析构函数进行inling”是一个很糟糕的想法。看下面这段代码:
Derived::Derived() { // 空白Derived构造函数"的观念性实现 Base::Base(); // 初始化Base成分 try{ dm1.std::string::string(); }catch(...){ Base::~Base(); throw; } try{ dm2.std::string::string(); }catch(...){ dm1.std::string::~string(); Base::~Base(); throw; } try{ dm3.std::string::string(); }catch(...){ dm2.std::string::~string(); dm1.std::string::~string(); Base::~Base(); throw; } }这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精致复杂的做法来处理异常。尽管如此,这已能准确反映Derived的空白构造函数必须提供的行为。
程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。
总结:
1)将大多数inlining限制在小型,被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2)不要因为function templates出现在头文件,就将它们声明为inline。
条款31:将文件间的编译依存关系降至最低
假设你对c++程序的某个class实现文件做了些轻微改变,修改的不是接口,而是实现,而且只改private成分。
然后重新建置这个程序,并预计只花数秒就好,当按下“Build”或键入make,会大吃一惊,因为你意识到整个世界都被重新编译和链接了!
问题是在c++并没有把“将接口从实现中分离”做得很好。class 的定义式不只详细叙述了class接口,还包括十足的实现细目:
class Person{ public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... private: std::string theName; //实现细目 Date theBirthDate; //实现细目 Address theAddress; //实现细目 };
这个class Person无法通过编译,Person定义文件的最上方可能存在这样的东西:
#include <string> #include "date.h" #include "address.h"这样写显然在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。可能就会导致开头我们提到的使你陷入窘境的情形出现。所以这里我们采取了另外一种实现方式,即将对象实现细则隐藏与一个指针背后。具体这样做:把Person类分割为两个类,一个只提供接口,另一个负责实现该接口。
#include <string> #include <memory> class PersonImpl; class Date; class Address; class Person{ public: Person(const std::string& name,const Date& birthday,const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... private: std::tr1::shared_ptr<PersonImpl> pImpl; }
这里,Person只内含一个指针成员,指向其实现类(PersonImpl)。这个设计常被称为pimpl idiom(pimpl是“pointer to implementation”的缩写)。
这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每件事都源自于这个简单的涉及策略。
1)如果用object reference 或 object pointer可以完成任务,就不要用objects。
2)如果能够,尽量以class声明式替换class定义式。
3)为声明式和定义式提供不同的头文件。
先Person这样使用pimpl idiom的classes,往往被称为Handle classes。
另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class称之为Interface class。这种class只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。一个针对Person而写的Interface class或许看起来像这样:
//Person.h ... using std::string; class Date; class Address; class Person { public: virtual ~Person(); virtual string name()const = 0; virtual string birthDate()const = 0; virtual string address()const = 0; ... static std::tr1::shared_ptr<Person> create(const string& name,const Date& birthday,const Address& addr); }; ... // person.cpp ... class RealPerson:public Person { public: RealPerson(const string& name,const Date& birthday,const Address& addr); virtual ~RealPerson(){} string name()const; ... private: string name_; Date theBirthDate_; Address theAddress_; }; std::tr1::shared_ptr<Person> Person::create(const string& name, const Date& birthday, const Address& addr){ return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr)); }Handle classes和Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。注意一点,两种class的实现方案带来的运行成本也是不容忽视的。如果你应该从你的实际出发,考虑用渐近方式去使用这些技术。
总结:
1)支持“编译依存性最小化”的一般构想是:相依于声明式,而不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
2)程序库头文件应该以“完全且仅有的声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
结语
虽然只有三个条款,但是篇幅比较长,逻辑相对而言也不是特别容易理解,在之后还应该继续多回顾回顾。
《Effective C++》学习笔记(八)