条款26 尽可能延后变量定义式的出现时间(Lazy evaluation)
记住:
★尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率
----------------------------------------------------------------------
举例说明:
std::string encryptPassword( const std::string &password ) { using namespace std; string encrypted; //过早定义变量“encrypted” if( password.length() < MininumLength ) { throw logic_error( "Password is too short" ); } ... return encrypted; }
一旦if语句中异常抛出,则上面的encrypted变量则未被使用,但仍得付出该变量的构造和析构成本,∴最好延后encrypted的定义式直到确实需要它。
更有甚,不只应延后变量的定义,甚至还应延后这份定义直到能给它初值为止,定义和赋初值一并完成,避免无谓的 先default构造 再赋初值。
对于循环的情况:
//方法A:定义于循环外 Widget w; for( int i=0; i<n; i++ ) { w = 取决于i的某个值; ... } //方法B:定义于循环内 for( int i=0; i<n; i++ ) { Widget w(取决于i的某个值); ... }
方法A 的成本:1个constructor + 1个destructor + n个assignment operate
方法B的成本:n个constructor + n个destructor
若赋值成本低于一组构造+析构成本,这样A大体而言交高效,否则B好。另外A造成w的作用域(覆盖整个循环)比B更大,有时那对程序的可理解性和易维护性造成冲突。
条款27 尽量少做转型动作
记住:
★若可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。若有个设计需要转型动作,试着发展无需转型的替代方案
★若转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内
★宁可使用C++风格转型(新式转型),不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌
---------------------------------------------------------------------------------------------------
两种旧式转型语法:
(T)expression
T(expression) //函数风格的转型动作
C++的四种新式转型:
const_cast<T>( expression ); //消除对象的常量性
dynamic_cast<T>( expression ); //安全向下转型(即向派生类端)
reinterpret_cast<T>( expression ); //执行低级转型
static_cast<T>( expression ); //强迫隐式转换
“我”(书作者)唯一使用旧式转型的情况是:当我要调用一个explicit构造函数(不允许隐式转换)将一个对象传递给一个函数时:
class Widget { public: explicit Widget( int size ); //ctor声明为explicit就不会导致隐式转换!!! ... }; void doSomeWork( const Widget &w ); doSomeWork( Widget(15) ); //以一个int加上“函数风格”的转型动作创建一个Widget
不要错误地认为转型其实什么都没做,只是告诉编译器把某个类型视为另一种类型。任何一个类型转化往往真的令编译器编译出运行期间执行的码。
------------------
关于转型的一个错误理解的例子:
假设有个Window基类和SpecialWindow派生类,进一步假设SpecialWindow的onResize被要求首先调用Window的onResize:
class Window { public: virtual void onResize() {...} ... }; class SpecialWindow : public Window { public: virtual void onResize() { static_cast<Window>(*this).onResize(); ... //这里进行SpecialWindow专属行为 } ... };
其实这句static_cast<Window>(*this).onResize();并非在当前对象身上调用Window::onResize之后又在该对象身上执行SpecialWindow专属动作。他是在“当前对象之base class成分”的副本上(!!!)调用Window::onResize,然后在当前对象身上执行SpecialWindow专属动作。这很有可能使当前对象出现“伤残”状态:其base class成分的更改没有落实,而derived class成分的更改倒是落实了。
解决方案如下:避免使用转换
class SpecialWindow : public Window { public: virtual void onResize() { Window::onResize(); ... } ... };
-----------------
除了对一般转型保持机敏与猜疑,更应该在注重效率的代码中对dynamic_cast保持机敏与猜疑。∵dynamic_cast的许多实现版本执行速度相当慢!!!
-----------------
之所以需dynamic_cast,通常是因为想在一个derived class对象身上执行derived class函数,但手上仅一个指向base的pointer或reference,有两个一般性做法:
做法一:使用容器并在其中存储直接指向derived class对象的指针(通常是smart pointer)
假设先前的Window/SpecialWindow继承体系中仅SpecialWindow才支持闪烁效果:
不要这样做: class Window {...}; class SpecialWindow : public Window { public: void blink(); ... }; typedef std::vector< std::tr1::shared_ptr<Window> > VPW; VPW winPtrs; ... for( VPW::iterator iter=winPtrs.begin(); iter != winPtrs.end(); iter++ ) { if( SpecialWindow *psw = dynamic_cast<SpecialWindow*>( iter->get() ) ) { psw->blink(); } } 而应这样做: typedef std::vector< std::tr1::shared_ptr<SpecialWindow> > VPSW; VPSW winPtrs; ... for( VPSW::iterator iter=winPtrs.begin(); iter != winPtrs.end(); iter++ ) { (*iter)->blink(); //这样写比较好,∵摆脱了dynamic_cast }
做法二:在base class内提供virtual函数做你想对各个Window派生类做的事:
class Window { public: virtual void blink() {} //缺省实现代码,啥也没做 //条款34将说明缺省实现代码可能是个馊主意 }; class SpecialWindow : public Window { public: virtual void blink(); ... }; typedef std::vector< std::tr1::shared_ptr<Window> > VPW; VPW winPtrs; ... for( VPW::iterator iter=winPtrs.begin(); iter != winPtrs.end(); iter++ ) { (*iter)->blink(); //也摆脱了dynamic_cast }
-------------------------------------------
绝对要避免“连串dynamic_cast”
typedef std::vector< std::tr1::shared_ptr<Window> > VPW; VPW winPtrs; ... for( VPW::iterator iter=winPtrs.begin(); iter != winPtrs.end(); iter++ ) { if( SpecialWindow1 *psw1 = dynamic_cast<SpecialWindow1*>(iter->get()) ) {...} else if( SpecialWindow2 *psw2 = dynamic_cast<SpecialWindow2*>(iter->get()) ) {...} else if( SpecialWindow3 *psw3 = dynamic_cast<SpecialWindow3*>(iter->get()) ) {...} ... }
这样产生出来的代码又大又慢,且基础不稳。
条款28 避免返回handles指向对象内部成分
记住:
★避免返回handles(包括reference,指针,迭代器)指向对象内部。遵守这个条款可增加封装性;帮助const成员函数的行为像个const;并将发生“虚吊号码牌”的可能性降到最低。
-------------------------------------------------------------------------
举例:
class Point { public: Point( int x, int y ); void setX( int newVal ); void setY( int newVal ); }; struct RectData { Point ulhc; Point lrhc; }; class Rectangle { public: ... Point& upperLeft() const { return pData->ulhc; } //有问题!!! ... private: std::tr1::shared_ptr<RectData> pData; };
上述代码虽可通过编译,却错误!!! ∵upperLeft返回reference指向private内部数据,调用者可以通过这些reference更改内部数据:
Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec( coord1, coord2 );
rect.upperLeft().setX(50); //更改了成员,但rec其实应该是const
从这个角度也可以看出上面定义的upperLeft虽然是个const但并未发挥作用!
解决方法:
class Rectangle { public: ... const Point& upperLeft() const { return pData->ulhc; } //返回类型加const ... };
客户只能读取矩形的Points,但不能涂写。这意味着当初定义upperLeft为const不再是谎言。
-------------------
即使如此,upperLeft返回了代表对象内部的handles,还有可能导致“空悬号码牌”:即这种handles所指东西不复存在(类似悬垂指针):
class GUIObject {...};
const Rectangle boundingBox( const GUIObject &obj );
客户如下使用:
GUIObject *pgo;
...
const Point* pUpperLeft = &( boundingBox(*pgo).upperLeft() );
一旦产出pUpperLeft的那个语句结束,pUpperLeft也就变为空悬、虚吊!!!
------------------
事物并非绝对的,有时需要让成员函数返回handles,如operator[ ]就允许你采摘strings和vectors的个别元素,而这些operator[ ]s就是返回references指向容器内的数据。但这毕竟是例外,非常态!
条款29 为“异常安全”而努力是值得的
记住:
★异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型
★“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
★函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者(短板效应)
---------------------------------------------------------
举例:
以下这个class用来表现夹带背景图案的GUI菜单,此class希望用于多线程环境,所以其有个互斥器(mutex)作为并发控制之用:
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 ); }
以上这个changeBackground函数不满足“异常安全”两个条件的任何一个:
不泄漏任何资源:上述一旦new Image( imgSrc )导致异常,互斥器就不会解锁了;
不允许数据败坏:若new Image( imgSrc )导致异常,bgImage就是指向一个已被删除的对象,imageChanges也已被累加,而其实并无新图像被成功安装。
上述对互斥器可能未被解锁的情况可以解决如下:
void PrettyMenu::changeBackground( std::istream &imgSrc ) { Lock ml( &mutex ); //条款14:获得互斥器并确保其稍后被释放 delete bgImage; ++imageChanges; bgImage = new Image( imgSrc ); }
-------------
异常安全函数提供以下三个保证之一:
基本承诺:若异常被抛出,程序内的任何事物仍保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
强烈保证:若异常被抛出,程序状态不改变。即若函数成功,就是完全成功,若函数失败,程序会恢复到调用函数之前的状态。
不抛掷(nothrow)保证:承诺绝不抛出异常,∵它们总是能够完成它们原先承诺的功能。作用于内置类型(如ints,指针等等)身上的所有操作都提供nothrow保证(即语言级保证)。
所谓异常安全码必须提供以上三种保证之一,若不这么做就不具备异常安全性。对大部分函数而言,往往选择前两者。
-----------------------------
copy and swap技术:此设计策略很典型地导致“强烈保证”。基本思想是为你打算修改的对象(原件)做出一份副本,然后在副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
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 ); //而pNew是智能指针,会自动消失 }
copy and swap技术关键在于“修改对象数据的副本,然后在一个不抛出异常的函数中将修改后的数据和原件置换”,∴必须为每一个即将被改动的对象做一个副本,这将耗费时间和空间,所以在现实中效率和复杂度将使得该方法并不是在何时都显得实际。
当“强烈保证”不切实际时,就必须提供“基本保证”。对许多函数来讲,“异常安全性之基本保证”也就够了!!!
-------------------------------
一个软件系统要不就具备异常安全性,要不就全然否定,无所谓的“局部异常安全系统”。若系统内有一个函数不具备异常安全性,整个系统就不具备异常安全性,因为调用那个不具备异常安全性的函数有可能导致资源泄露或数据结构败坏(短板效应)。
条款30 透彻了解inlining的里里外外
记住:
★将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可以使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
★不要只因为function templates出现在头文件,就将它们声明为inline
---------------------------------------------------------------------------------------------
inline函数的观念是:对此函数的每一个调用都以函数本体替换之。但这样做可能会增加目标码的大小。在一台内存有限的机器上,过度热衷inlining会造成程序体积太大。即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随这些而来的效率损失。
inline仅是对编译器的一个申请,不是强制命令。
声明inline有隐式和显式两种方式:前者是将函数定义于class定义式内(friend函数也可被定义于class内,若真是那样它们也是被隐喻声明为inline);后者则是直接加上关键字inline。
---------------------
解决一个疑惑:
我们发现inline函数和templates两者通常都是被定义于头文件内。这使得某些程序员以为function templates一定必须是inline,这个观点是错误的!!!
∵inline函数通常一定被置于头文件内是因为大多数建置环境在编译过程中进行inlining;而templates通常也被置于头文件是因为它一旦被使用,编译器为了将其具现化,需要知道它长什么样子(也即需要它的详细定义)。
可见template的具现化与inline无关!!!
---------------------
所有对virtual函数的调用(除非是最平淡无奇的)会使inlining落空。因为virtual意味着等到运行期才确定调用哪个函数。
编译器通常不对通过函数指针而进行的调用实施inlining(但也不绝对):
inline void f() {...}
void (*pf)() = f;
...
f(); //此调用将会被inlining
pf(); //这个调用或许不被inlining,∵其通过函数指针达成
---------------------
大部分调试器面对inline函数束手无策。
---------------------
80-20经验法则:
平均而言一个程序往往将80%的执行时间花费在20%的代码上头。此法则提醒我们,目标是找出这可以有效增进程序整体效率的20%代码,然后将它inline或竭尽所能地将它瘦身。
条款31 将文件的编译依存关系降至最低
记住:
★支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
★程序库头文件应以“完全且仅有声明式”形式存在。此法不论是否涉及templates都适用。
-------------------------------------------------------------------------------------
何为Handle class:
handle class往往指使用了pimpl(pointer to implementation) idiom(习语)的class。
问题背景:
#include<string> #include "date.h" //这就形成了一个编译依存关系!!!很要命 #include "address.h" 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; //成员变量也属于一个class的实现细目!!! Date theBirthDate; //一旦此成员变量类的定义有变,会导致Person Address theAddress; //的代码必须重新编译,此即编译依存!!! };
解决办法:“将对象实现细目隐藏于一个指针背后”
针对上面,可以把Person分割为两个classes,一个只提供接口,另一个负责实现该接口(桥接模式!!!)。若负责实现的那个implementation class名为PersonImpl,Person将定义如下:
#include <string> #include <memory> //tr1::shared_ptr需要 class PersonImpl; //Person实现类的前置声明,无需包含头文件 class Date; //Person接口用到的classes的前置声明 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只内含一个指针指向其实现类,这种设计被称为pimpl idiom。此设计下,Person的客户就完全与Date,Address以及Person的实现细目分离了。那些class的任何实现修改都无需Person客户端重新编译。此即“接口与实现分离!!!!”。
几个策略:
♢ 若使用object references或object pointers可以完成任务(∵这样就仅需一个类型前置声明,无需包含头文件),就不要使用objects;
♢ 若能够,尽量以class声明式替换class定义式
当声明一个函数而用到某class时,是无需该class定义的;纵使函数以by value方式传递该类型的参数亦然:
class Date; //仅此前置声明足够
Date today(); //此处无需Date的定义式,就上一句的前置声明足够
void clearAppointments( Date d ); //同上
♢ 为声明式和定义式提供不同的头文件
上面Person这样的handle class如何真正做事情:办法之一是将它们的所有函数转交给相应的实现类并由后者完成实际工作。例如下面是Person两个成员函数的实现:
#include "Person.h" #include "PersonImpl.h" //必须包含此头文件,否则无法调用其成员函数;注意PersonImpl有着和Person完全相同的成员函数,两者接口完全相同 Person::Person( const std::string &name, const Date &birthday, const Address &addr ) : pImpl( new PersonImpl( name, birthday, addr ) ) { } std::string Person::name() const { return pImpl->name(); //可见PersonImpl和Person的接口完全相同!!! }
------------------------
Interface class
针对上面的Person而言,即令Person为一种特殊的抽象基类:
class Person { public: virtual ~Person(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; virtual std::string address() const = 0; ... };
针对interface class,往往在其中声明一个工厂函数(且往往static):
class Person { public: ... static std::tr1::shared_ptr<Person> create( //静态非虚! const std::string &name, const Date &birthday, const Address &addr ); ... };
客户会这样使用它们:
string name; Date dateOfBirth; Address address; ... //创建一个对象支持Person接口(且是sp) std::tr1::shared_ptr<Person> pp( Person::create( name, dateOfBirth, address ) ); ... std::cout << pp->name() << ...; ...
若interface class Person有个具象的子类RealPerson,这样的话写出Person::create就像这样:
std::tr1::shared_ptr<Person> Person::create( const std::string &name, const Date &birthday, const Address &addr ) { return std::tr1::shared_ptr<Person>( new RealPerson( name, birthday, addr ) ); }
当然更现实的Person::create实现代码会创建不同类型的派生类对象,取决于诸如额外参数值、读自文件或数据库的数据、环境变量等。