大多数情况下,适当提出你的classes(和class templates)定义以及functions(和function templates )声明,是花费最多心力的两件事。一旦正确完成它们,相应的实现大多直截了当。尽管如此,还是有些东西需要小心。
太快定义变量可能造成效率上的拖延.
过度使用转型(casts )可能导致代码变慢又难维护,又招来微妙难解的错误.
返回对象“内部数据之号码牌(handles )”可能会破坏封装并留给客户虚吊号码牌(dangling handles).
未考虑异常带来的冲击则可能导致资源泄漏和数据败坏.
过度热心地inlining可能引起代码膨胀;过度耦合(coupling)则可能导致让人不满意的冗长建置时间(build times )。
Rule 26 尽可能延后变量定义式的出现时间
只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流(control flow)到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本,所以你应该尽可能避免这种情形。
//这个函数过早定义变量”ncrypted" std::string encryptPassword(const std::string& password) { using namespace std; string encrypted; if (password.length()<MinimumPasswordLength){ throw logic_error("Password is too short"); } //必要动作,稗能将一个加密后的密码 //置入变量encrypted内 return encrypted; }
以上的函数中encrypted变量存在过早构造(默认构造函数)。在if控制流可能就return了,不会用到encrypted。
std::string encryptPassword(const std::string& password) { using namespace std; if (password.length()<MinimumPasswordLength){ throw logic_error("Password is too short"); } string encrypted; return encrypted; }
以上优化第一步:
std::string encryptPassword(const std::string& password) { //... //检查长度。 std::string encrypted(password);//通过copy构造函数 //定义并初始化。 encrypt(encrypted); return encrypted; }
以上是最佳优化,在需要使用的时候在定义。
-
在for循环的时候。
//方法A:定义于循环外 Widget w; for (int i=0; i<n; ++i) // w=取决于i的某个值; //方法B:定义于循环内 for (int i=0;Widget<n; ++i) //Widget w(取决于i的某个值);
-
做法A: 1个构造函数+1个析构函数十n个赋值操作.
做法B: n个构造函数+n个析构函数
那么问题就在于考虑构造和析构函数和赋值函数哪个成本更高。
-
尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
Rule 27 尽量少做转型动作
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。理论上如果你的程序很“干净地”通过编译,就表示它并不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作。这是一个极具价值的保证,可别草率地放弃它。
C风格的转型动作看起来像这样:
(T)expression//将expression转型为T函数风格的转型动作看起来像这样:T(expression)//将expression转型为T
C++还提供四种新式转型(常常被称为new-style或C++style casts):
const_cast<T>(expression)dynamic_cast<T>(expression)reinterpret_cast<T>(expression)static_cast<T>(expression)
-
const_cast通常被用来将对象的常量性转除(cast away the constness )。它也是唯一有此能力的C++-style转型操作符。
-
dynamic_cast主要用来执行“安全向下转型”( safe downcasting ),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作(稍后细谈)。
-
reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。这一类转型在低级代码以外很少见。本书只使用一次,那是在讨论如何针对原始内存(raw memory)写出一个调试用的分配器(debugging allocator)时,见条款50.
-
static_cast用来强迫隐式转换(implicit conversions ),例如将non-const对象转为const对象(就像条款3所为),或将int转为 double等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const一一这个只有const cast才办得到。
许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。这是错误的观念。任何一个类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。
int x, y; double d=static_cast<double>(x)/y; //x除以y,使用浮点数除法
将int x转型为double几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int的底层表述不同于double的底层表述。这或许不会让你惊讶,但下面这个例子就有可能让你稍微睁大眼睛了:
class Base{…}; class Derived: public Base{ Derived d; Base* pb=&d; //隐喻地将Derived*转换为Base* };
使用父类指针指向子类对象。
假设我们有个Window base class和一个Specialwindow derived class,两者都定义了virtual函数onResize()进一步假设SpecialWindow的onResize函数被要求首先调用Window的onResize。下面是实现方式之一,它看起来对,但实际上错:
class Window{//base class public: virtual void onResize(){...}//base onResize }; class SpecialWindow: public Window{//derived class public: virtual void onResize();//derived onResize实现代码 static_cast<Window>(*this).onResize(); / //然后调用其。nResize; //这不可行! //这里进行SpecialWindow专属行为。 }
而这样调用实际的效果是它会产生一个临时的Window对象。对该对象调用了onResize();而剩下的则是针对当前对象的操作。这样子造成了真实对象base部分没有修改,而Derived部分被修改了。
class SpecialWindow: public Window{ public: virtual void onResize(); Window::onResize(); //调用Window::onResize作用于*this身上 };
在探究dynamic_cast设计意涵之前,值得注意的是,dynamic_cast的许多实现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于,"class名称之字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行dynamic_cast刚才说的那个实现版本所提供的每一次dynamic cast可能会耗用多达四次的strcmp调用,用以比较class名称。
dynamic_cast的速度感人,所以以下有两个办法去规避。
-
使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针,见条款13),如此便消除了“通过base class接口处理对象”的需要。假设先前的Window/SpecialWindow继承体系中只有SpecialWindows才支持闪烁效果,试着不要这样做:
class Window{…}; class SpecialWindow: public Window{ public: void blink(); typedef//关于trl::shared ptr std::vector<std::tr1::shared_ptr<Window>>VPW;//见条款13. VPW winPtrs; for (VPW::iter =winPtrs.begin(); iter !=winPtrs.end();++iter){ //不希望使用 //dynamic_cast. if (SpecialWindow*psw=dynamic_cast<SpecialWindow*>liter->get())) psw->blink(); } };
应该改成这样:
typedef std::vector<std::trl::shared}tr<SpecialWindow>>VPSW VPSW winPtrs; for(VPSW::iterator iter=winPtrs.begin(); iter!=winPtrs.end(); ++iter) //这样写比较好, //不使用dynamic_cast (*iter)->blink();
这种做法使你无法在同一个容器内存储指针“指向所有可能之各种Window派生类”。如果真要处理多种窗口类型,你可能需要多个容器,它们都必须具备类型安全性(type-safe)。
-
另一种做法可让你通过base class接口处理“所有可能之各种Window派生类”,那就是在base class内提供virtual函数做你想对各个Window派生类做的事。
class Window{ public: virtual void blink(){} //缺省实现代码“什么也没做”; //条款34告诉你为什么 //缺省实现代码可能是个嫂主意。 }; class SpecialWindow: public Window{} public: virtual void blink(){…}; //在此class内, //blink做某些事。 typedef std::vector<std::trl::shared ptr<Window>>VPW; VPW winPtrs;//容器,内含指针,指向 //所有可能的Window类型。 for (VPW::iterator iter=winPtrs.begin(); iter!=winPtrs.end(); ++iter) (*iter)->blink(); //注意,这里没有 //dynamic casto };
相当于作为虚函数,利用多态去实现。
绝对必须避免的一件事是所谓的“连串(cascading) dynamic- casts",也就是看起来像这样的东西:
class Window typedef std::vector<std::trl::shared_ptr<Window>>VPW; VPW winPtrs; //derived classes定义在这里 for (VPW::iterator iter=winPtrs.begin(); iter!=winPtrs.end();++iter) { if (SpecialWindowl*pswl= dynamic cast<SpecialWindowl*>(iter->get(》)) else if (SpecialWindow2*psw2= dynamic cast<SpecialWindow2*>(iter->get())) else if (SpecialWindow3*psw3= dynamic cast<SpecialWindow3*>(iter->get())) } };
-
如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic- casts.如果有个设计需要转型动作,试着发展无需转型的替代设计。
-
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
-
宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
Rule 28 避免返回handles指向对象内部成分
handles就是指针的意思。如果我们返回一个对象内部的引用相当于我们可以修改该对象的信息,不管返回类型是否是const。
eg:
-
返回了handles,获得了修改对象内部的权限。
class Point{//这个class用来表述“点” public: Point(int x, int y); //... //... void setX(int newVal); void setY(int newVal); }; struct RectData{ Point ulhc; Point lrhc; }; class Rectangle{ //这些“点”数据用来表现一个矩形 //ulhc="upper left-hand comer"(左上角) //lrhc = "lower right-hand comer"(右下角) private: std::trl::shared_ptr<RectData> pData; //关于trl::shared}tr} };//见条款13 class Rectangle{ public: Point& upperLeft()const{return pData->ulhc;} Point& lowerRight()const{return pData->lrhc;} }; Point coordl(0, 0); Point coord2(100, 100); const Rectangle rec(coordl, coord2);//rec是个const矩形, //从(0,0)到(100,100) rec.upperLeft().setX(50);//现在·rec却变成 从(50,0)到(100,100)
上面我们所说的每件事情都是由于“成员函数返回references"。如果它们返回的是指针或迭代器,相同的情况还是发生,原因也相同。References、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。同时,一如稍早所见,它也可能导致“虽然调用const成员函数却造成对象状态被更改”。
修改如下:
class Rectangle{ public: const Point& upperLeft()const{return pData->ulhc;} const Point& lowerRight()const{return pData->lrhc;} };
对引用加上const,使得其没有修改权限。
-
可能导致dangling handles(空悬的号码牌)。
这种情况的意思是相对应的对象可能被析构掉了,但是客户还持有该对象内部资源的引用。
-
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const.并将发生“虚吊号码牌”< dangling handles)的可能性降至最低。
Rlue 29 为“异常安全”而努力是值得的
假设有个class用来表现夹带背景图案的GUI菜单单。这个class希望用于多线程环境,所以它有个互斥器(mutex)作为并发控制(concurrency control)之用:
class PrettyMenu{ public: void changeBackground(std::istream& imgSrc);//改变背景图像 private: Mutex mutex;//互斥器 Image* bgImage;//目前的背景图像 int imageChanges;//背景图像被改变的次数 };
下面是PrettyMenu的changeBackground函数的一个可能实现:
void PrettyMenu::changeBackground(std::istream& imgSrc) { lock(&mutex);//取得互斥器(见条款14) delete bgImage;//摆脱旧的背景图像 ++imageChanges;//修改图像变更次数 bgImage = new Image(imgSrc);//安装新的背景图像 unlock(&mutex);//释放互斥器 }
到这里可以发现问题,如果在lock到unlock中有调用失败。(delete失败,则unlock得不到调用)
异常安全条件:
-
不泄漏任何资源。上述代码没有做到这一点,因为一旦”new Image(imgSrc)”导致异常,对unlock的调用就绝不会执行,于是互斥器就永远被把持住了.
-
不允许数据败坏。如果”new Image(imgSrc)”抛出异常,bgImage就是指向一个己被删除的对象,imageChange。也己被累加,而其实并没有新的图像被成功安装起来。
解决资源泄漏的问题很容易,因为条款13讨论过如何以对象管理资源,而条款14也导入了Lock class作为一种“确保互斥器被及时释放”的方法:
void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex);//来自条款14:获得互斥器并确保它稍后被释放 不允许复制 delete bgImage; ++imageChanges; bgImage=new Image(imgSrc); }
异常安全函数(Exception-safe functions)提供以下三个保证之一:
-
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态(exact state )恐怕不可预料。举个例子,我们可以撰写changeBackground使得一旦有异常被抛出时,PrettyMenu对象可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么。
-
强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
-
不抛掷(nothrow)保证,承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。
为了提供强烈保证。首先把PrettyMenu中的指针Image对象指针改为shared_ptr.其次,重新排列changeBackground内的语句次序。保证在new成功之后才对Image++.
class PrettyMenu{ std::tr1::shared_ptr<Image> bgImage; //... }; void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex); bgImage.reset (new Image(imgSrc));//以,'new工mage',的执行结果 //设定bg工mage内部指针 ++imageChanges; }
注意,这里不再需要手动delete旧图像,因为这个动作己经由智能指针内部处理掉了。此外,删除动作只发生在新图像被成功创建之后。更正确地说,trl:.shared_ptr::reset函数只有在其参数(也就是”new Image(imgSrc)”的执行结果)被成功生成之后才会被调用。delete只在reset函数内被使用,所以如果从未进入那个函数也就绝对不会使用delete。也请注意,以对象(trl::shared_ptr)管理资源(这里是动态分配而得的工mage)再次缩减了changeBackground的长度。
有个一般化的设计策略很典型地会导致强烈保证,很值得熟悉它。这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap )。
struct PMImpl{ std::tr1::shared_ptr<Image> bgImage; int imageChanges;//稍后说明为什么它是个struct }; class PrettyMenu{ private: Mutex mutex; std::tr1::shared_ptr<PMImpl> pImpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc){ using std::swap; Lock ml(&mutex); std::trl::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //见条款25 //获得mutex的副本数据 phew->bgImage.reset(new Image(imgSrc)); ++pNew->imageChanges; swap(pImpl, phew); //修改副本 //置换(swap)数据,释放 }
此例之中我选择让PMImpl成为一个struct而不是一个class,这是因为PrettyMenu的数据封装性己经由于“pImpl是private'而获得了保证。
"copy-and-swap”策略是对对象状态做出“全有或全无”改变的一个很好办法,但一般而言它并不保证整个函数有强烈的异常安全性。为了解原因,让我们考虑changeBackground的一个抽象概念:someFunc。它使用copy-and-swap策略,但函数内还包括对另外两个函数fl和f2的调用:
void someFunc() { //对local状态做一份副本 fl(); f2(); //将修改后的状态置换过来 ... }
someFunc的异常级级别取决于整个函数下限。
-
异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
-
“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
-
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
Rule 30 透彻了解inlining的里里外外
inline
优点:动作像函数,比宏好得多(见条款2),可以调用它们又不需蒙受函数调用所招致的额外开销。
缺点:将“对此函数的每一个调用”都以函数本体替换之。增加你的目标码(object code)大小。导致命中下降,降低执行速度。
记住,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内:
class Person{ public: int age()const { return theAge;} //一个隐喻的inline申请: //age被定义于class定义式内。 private: int theAge; };
明确声明inline函数的做法则是在其定义式前加上关键字inline。例如标准的max template(来自<algorithm>)往往这样实现出来:
template<typename T> //明确申请inline: inline const T& std::max(const T& a, const T& b)//std::max之前有 {return a<b?b:a;}//关键字,"inline"
-
对于复杂的函数--循环,递归,编译器会拒绝。
-
对于virtual函数,编译器一律拒绝。因为Inline是编译期的行为,virtual是运行期的行为。
inline void f(){…}//假设编译器有意愿inline“对f的调用” void (*pf)()=f; //pf指向f f();//这个调用将被inlined,因为它是一个正常调用。 pf();//这个调用或许不被inlined,因为它通过函数指针达成。
构造函数和析构函数往往是inlining的糟糕候选人.考虑以下Derived class构造函数:
编译器会生成部分代码。
class Base{ public: private: std::string bml, bm2; //base成员1和2 }; class Derived: public Base{ public: Derived(){} //Derived构造函数是空的,哦,是吗? private: std::string dml, dm2, dm3; //derived成员1-3 }; Derived::Derived()//“空白Derived构造函数”的观念性实现 { Base::Base();//初始化“Base成分,, try{dml,std::string::string();}//试图构造dmlo catch(…){//如果抛出异常就 Base::~Base();//销毁base class成分,并 throw;//传播该异常。 } try{dm2.std::string::string();}//试图构造dm2a catch(...){//如果抛出异常就 dml .std::string::~string();//销毁dml, Base::~Base();//销毁base class成分,并 throw;//传播该异常。 } try{dm3 .std::string::string(); catch(…){ dm2.std::string::~string( dm1.std::string::~string( Base::~Base(); throw; //试图构造dm3o //如果抛出异常就 //销毁山它, //销毁dml, //销毁base class成分,并 //传播该异常。 }
-
将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。必须重新编译。
-
在Inline函数内无法设置断点。
-
将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
-
不要只因为function templates出现在头文件,就将它们声明为inline。
Rule 31 将文件间的编译依存关系降至最低
正常我们写的代码往往相互之间耦合紧密,一个是头文件之间耦合非常紧密。如果一个头文件改动了,相关依赖的文件全部都需要重新编译。第二个是类之间的相互依赖,在复合类型中,如果其中一个类改变了,意味着相关依赖的类需要重新编译。
解决的方法:
-
针对标准的库文件,使用预编译头文件。(把部分库的文件编译好)
-
针对类之间相互依赖性,使用接口和实现分离的方式。
//标准程序库组件不该被前置声明。 //trl::shared ptr而含入;详后。 class PersonImpl;//Person 实现类前置声明 class Date;// class Address; //Person实现类的前置声明。 //Person接口用到的dasses的前置声明。 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::trl::shared ptr<PersonImpl> pImpl; //指针,指向实现物; //std:trl:shared_ptr见条款13 };
在这里,main class( Person)只内含一个指针成员(这里使用trl : : shared_ptr见条款13),指向其实现类(PersonImpl)。这般设计常被称为pimpl idiom .
-
如果使object references或object pointers可以完成任务,就不要使用objects.你可以只靠一个类型声明式就定义出指向该类型的references和pointers.但如果定义某类型的,就需要用到该类型的定义式。
-
如果能够,尽量以class声明式替换class定义式。注意,当你声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以by value方式传递该类型的参数(或返回值)亦然。
-
为声明式和定义式提供不同的头文件。
像Person这样使用pimpl idiom的classes,往往被称为Handle classes将它们的所有函数转交给相应的实现类(implementation classes)并由后者完成实际工作。例如下面是Person两个成员函数的实现:
//我们正在实现Person class> //所以必须#include其class定义式。 //洲门也必须#include Person工叩1的 //class定义式,否则无法调用其成员函数; //注意,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(); }
第二种是采取接口的方式实现(Interface class):
class Person{ public: virtual~ Person(); virtual std::string name()const,0; virtual std::string birthDate()const=0; virtual std::string address()const=0; };
当然,该方式还必须要有一个特殊的函数创建class对象。往往称之为factory函数。在Interface class中声明为static:
static: class Person{ public: static std::trl::shared ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr); //返回一个trl::shared_ptr,指向 //一个新的Person,并以给定之参数 //初始化。条款1s告诉你 //为什么返回的是trl::shared ptr };
-
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes.
-