条款26:尽可能延后变量定义式的出现时间
假设定义一个函数,它计算通行密码的加密版本而后返回,前提是密码够长。如果密码太短,函数会丢出一个异常,类型为logic_error:
//这个函数过早定义变量“encrypted”
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在此函数中并未被使用,但如果有个异常被丢出,它就真的没有被使用。也就是说如果函数encryptPassword丢出异常,你仍得付出encrypted的构造和析构成本。因此,延后encrypted的定义,直到确实需要它:
//这个函数延后定义变量“encrypted”,直到真正需要它
std::string encryptPassword(const std::string& password){
using namespace std;
if(password.length()<MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
string encrypted;
//... 必要动作,俾能将一个加密后的密码 置入变量encrypted内
return encrypted;
}
上面代码不够秾纤合度,因为encrypted虽然获得定义却无任何实参作为初值。很多时候你该对对象做的第一次事情就是给它一个初值。
假设,encryptPassword的艰难部分在encrypt函数进行。
void encrypt(std::string& s);//在其中的适当地点对s加密
//这个函数延后定义变量“encrypted”,直到真正需要它
//但此函数仍然有着不该有的效率低落
std::string encryptPassword(const std::string& password){
using namespace std;
if(password.length()<MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
std::string encrypted;//default-construct
encrypted =password; //赋值给encrypted
encrypt(encrypted);
return encrypted;
}
更受欢迎的做法是以password作为encrypted的初值,跳过毫无意义的默认构造函数。
void encrypt(std::string& s);//在其中的适当地点对s加密
std::string encryptPassword(const std::string& password){
using namespace std;
if(password.length()<MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
std::string encrypted(password);//copy-construct定义并初始化
encrypt(encrypted);
return 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的某个值);
...
}
在Widget函数内部,以上两种写法的成本如下:
A:1个构造函数 +1个析构函数 +n个赋值操作
B:n个构造函数+n个析构函数
如果class的一个赋值成本低于一组构造+析构成本,做法A大体而言比较高效。尤其当n值很大的时候。否则做法B或许更好。此外做法A造成名称w的作用域比做法B更大,有时候那对程序的可理解性和易维护性造成冲突。除非(1)你知道赋值成本比“构造+析构”成本低,(2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法B。
请记住:
1、尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
条款27:尽量少做转型动作
C风格的转型动作看起来像这样:
(T)expression //将expression转型为T
函数风格的转型看起来像这样:
T(expression) //将expression转型为T
两种形式无差别,只是小括号位置不一样。这种就是“旧式转型”。
C++还提供了四种新式转型(C++ style(新式)):
const_cast<T>(expression);
dynamic_cast<T>(expression);
reinterpret_cast<T>(expression)
static_cast<T>(expression)
1、const_cast通常被用来将对象的常量性转除。
2、dynamic_cast主要用来执行“安全向下转型(父类转子类有安全性检查)”,也就是用来决定某个对象是否归属继承体系中的某个类型。
3、reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如,将pointer to int转型为一个int。
4、static_cast用来强迫隐性转换,例如,non-const转const,将int转double,将void* 转typed指针,将pointer-to-base转pointer-to-derived。但无法将const转non-const——这个只有const_cast才能办到。
当我要调用一个explicit构造函数将一个对象传递给一个函数时。例如:
class Widget{
public:
explicit Widget(int size);
//...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15));//以一个int加上“函数风格”的转型动作创建一个Widget.
doSomeWork(static_cast<Widget>(15));//以一个int加上“C++风格”的转型动作创建一个Widget.
假设我们有个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();//将*this转型为Window,然后调用其
//onResize;这不可行
//... //这里进行SpecialWindow专属行为
}
//...
};
这段程序将* this转型为Window,对函数onResize的调用也因此调用了Window::onResize。它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“ * this对象之base class成分”的暂时副本身上的onResize。当前对象之base class成分的副本上调用Window::onResize,然后再当前对象身上执行SpecialWindow专属动作。如果Window::onResize修改了对象内容,当前对象其实没被改动,改动的是副本。
如果你想调用base class 版本的onResize函数,令它作用于当前对象身上,因此修改如下:
class SpecialWindow:public Window{ //derived class
public:
virtual void onResize(){ //derived onResize实现代码
Window::onResize();//调用Window::onResize()作用于* this身上
//... //这里进行SpecialWindow专属行为
}
//...
};
之所以需要dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上却只有一个“指向base”的pointer或reference,你只能它们来处理对象。有两个一般性做法可以避免这个问题。
第一,使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class 接口处理对象”的需要。假设先前的Window/SpecialWindow继承体系中只有SpecialWindow才支持闪烁效果,试着不要这样做:
class Window{
//...
};
class SpecialWindow:public Window{
public:
void blink();
//...
};
typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;
//...
for (VPW::iterator iter=winPtrs.begin(); //不希望使用
iter !=winPtrs.end();++iter) { //dynamic_cast
if(SpecialWindow* psw=dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink();
}
应该改成这样做:
typedef std::vector<std::shared_ptr<SpecialWindow>> VPSW;
VPSW winPtrs;
//...
for (VPSW::iterator iter=winPtrs.begin(); //这样写比较好
iter !=winPtrs.end();++iter) { //不使用dynamic_cast
(*iter)->blink();
}
这种做法无法在同一个容器内存储指针“指向所有可能之各种Window派生类”。如果真要处理多种窗口类型,你可能需要多个容器,它们都必须具备类型安全性。
第二种做法是:通过base class接口处理“所有可能之各种Window派生类”,那就是base class 内提供virtual 函数做你相对各个Window派生类做的事情。举个例,虽然只有SpecialWindow可以闪烁,但或许闪烁函数声明于base class 内并提供一份“什么也没做”的缺省代码是有意义的:
class Window{
virtual void blink();//虚函数,缺省实现代码“什么也都没做”;缺省实现可能是个馊主意
//...
};
class SpecialWindow:public Window{
public:
virtual void blink();//在此class 内,blink做某些事
//...
};
typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;//容器,内含指针,指向所有可能的Window类型
//...
for (VPW::iterator iter=winPtrs.begin(); //这样写比较好
iter !=winPtrs.end();++iter) { //不使用dynamic_cast
(*iter)->blink();//注意 这里没有dynamic_cast
}
无论哪一种写法——“使用类型安全容器”或“将virtual函数往继承体系上方移动”——都并非放之四海皆准,但在许多情况下它们都提供一个可行的dynamic_cast替代方案。当它们有此功效时,你应该欣然拥抱它们。
绝对必须避免的一件事是所谓的“连串dynamic_cast”,也就是看起来像这样的东西:
class Window{//...};
...//derived class 定义放在这里
typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;//容器,内含指针,指向所有可能的Window类型
//...
for (VPW::iterator iter=winPtrs.begin(); //这样写比较好
iter !=winPtrs.end();++iter) { //不使用dynamic_cast
if(SpecialWindow1* psw=dynamic_cast<SpecialWindow1*>(iter->get()){){
//...
}
else if(SpecialWindow2* psw=dynamic_cast<SpecialWindow2*>(iter->get()){){
//...
}
else if(SpecialWindow3* psw=dynamic_cast<SpecialWindow3*>(iter->get()){){
//...
}
//...
}
这样产生出来的代码又大又慢,而且基础不稳,因为每次Window class继承体系一有改变,所有这一类代码都必须再次检阅是否需要修改。
优良的C++代码很少使用转型,但若说要完全摆脱它们又太过不切实际。
请记住:
1、如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
2、如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
3、宁可使用C++ style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
条款28:避免返回handles指向对象内部成分
假设你的程序涉及矩形。每个矩形由其左上角和右下角表示。
class Point{//这个class用来表述“点”
public:
Point(int x,int y);
//...
void setX(int newVal);
void setY(int newVal);
//...
};
struct RectData{//这些 “点”数据用来表现一个矩形
Point ulhc;//ulhc="upper left-hand corner"(左上角)
Point lrhc;//lrhc="lower right-hand corner"(右下角)
};
class Rectangle{
//...
private:
std::shared_ptr<RectData> pData;
Point& upperLeft() const { return pData->ulhc;}
Point& lowerRight() const { return pData->lrhc;}
};
这样的设计可以通过编译,但却是错误的。实际上是自我矛盾的。一方面upperLeft 和lowerRight被声明为const成员函数,因为它们的目的只是为了提供客户一个得知Rectangle相关坐标的方法,而不是让客户去修改Rectangle。另一个方面两个函数却都返回reference指向private内部数据,调用者于是可以通过reference更改内部数据!例如:
Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1,coord2);//rec 是个const矩形,从(0,0)到(100,100)
rec.upperLeft().setX(50);//现在rec却变成从(50,0)到(100,100)
这里注意,upperLeft的调用着能够使用被返回的reference来更改成员。但rec其实应该是不可变的(const)!
这里有两个教训。第一,成员变量的封装性最多只等于“返回其reference”的函数访问级别。第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
Reference、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是”降低对象封装性“的风险。
我们在这些函数身上遭遇的两个问题可以轻松去除,只要对它们的返回类型加上const即可:
class Rectangle{
//...
private:
std::shared_ptr<RectData> pData;
const Point& upperLeft() const { return pData->ulhc;}
const Point& lowerRight() const { return pData->lrhc;}
};
这些函数只让渡读取权。涂写权仍然是被禁止的。
假若某个函数返回GUI对象的外框(bounding box),这个外框采用矩形形式:
class GUIObject{
//...
};
const Rectangle boundingBox(const GUIObject& obj);//以by value方式返回一个矩形
//现在客户有可能这么使用这个函数:
GUIObject *pgo;//让pgo指向某个GUIObject
//...
const Point* pUpperLeft=&(boundingBox(*pgo).upperLeft());//取得一个指针指向外框左上角
}
对boundingBox的调用获得一个新的、暂时的Rectangle对象。这个对象没有名称,所以我们权且称它为temp。随后upperLeft作用于temp身上,返回一个reference指向temp的一个内部成分,更具体地说是指向一个用以标示temp的Point。于是pUpperLeft指向那个Point对象。在那个语句结束之后,boundingBox的返回值,也就是我们所说的temp,将被销毁,而那间接导致temp内的Point析构。最终导致pUpperLeft指向一个不再存在的对象:也就是说一旦产出pUpperLeft的那个语句结束,pUpperLeft也就变成空悬、虚吊(dangling)!
请记住:
1、避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌(dangling handles)”的可能性降至最低。
条款29:为“异常安全”而努力是值得的
假设有个class用来表现夹带背景图案的GUI菜单单。这个class希望用于多线程环境,所以需要互斥器作为并发控制。
class PrettyMenu{
public:
//...
void changeBackground(std::iostream& imgSrc);//改变背景图像
private:
Mutex mutex;//互斥器
Image* bgImage;//目前的背景图像
int imageChanges;//背景图像被改变的次数
};//
//下面是PrettyMenu的changeBackground函数的一个可能实现:
void PrettyMenu::changeBackground(std::iostream &imgSrc)
{
lock(&mutex);//取得互斥器
delete bgImage;//摆脱旧的背景图像
++imageChanges;//修改图像变更次数
bgImage=new Image (imgSrc);//安装新的背景图像
unlock(&mutex);//释放互斥器
}
在“异常安全性”方面,这个函数很糟。
当异常被抛出时,带有异常安全性的函数会:
1、不泄漏任何资源。上述函数没有做得,因为一旦“new Image(imgSrc)”导致异常,对unlock的调用就绝不会执行,于是互斥器就永远被把持住了。
2、不允许数据败坏。
间接资源泄漏问题的方法,修改为如下:
void PrettyMenu::changeBackground(std::iostream &imgSrc)
{
Lock ml(&mutex);//获得互斥器并确保它稍后被释放
delete bgImage;//摆脱旧的背景图像
++imageChanges;//修改图像变更次数
bgImage=new Image (imgSrc);//安装新的背景图像
}
有一个一般性规则是这么说的,较少的代码就是较好的代码,因为出错就会比较少,而且一旦有所改变,被误解的机会也就比较少。
任何使用动态内存的东西(例如所有STL容器)如果无法找到足够内存以满足需求,通常便会抛出一个bad_alloc异常。
如果修改成提供强烈保证。首先改变PrettyMenu的bgImage成员变量的类型,从一个Image* 内置类型指针改为一个“用于资源管理”的智能指针。其下策略是:不要为了表示某个事情发生而改变对象状态,除非那件事情真的发生了。代码如下
class PrettyMenu{
public:
//...
void changeBackground(std::iostream& imgSrc);//改变背景图像
private:
//...
std::shared_ptr<Image> bgImage;//目前的背景图像
//...
int imageChanges;//背景图像被改变的次数
};//
//下面是PrettyMenu的changeBackground函数的一个可能实现:
void PrettyMenu::changeBackground(std::iostream &imgSrc)
{
lock(&mutex);//取得互斥器
bgImage.reset(new Image (imgSrc) );//以“new Image”的执行结果设定bgImage内部指针
++imageChanges;
}
这里不需要手动delete旧图像,因为这个动作已经由智能指针内部处理了。如果Image构造函数抛出异常,有可能输入流的读取记号已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。
有个一般化的设计策略很典型地会导致强烈保证,很值得熟悉它。这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那个副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。
实现上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种手法常被pimpl idiom,对PrettyMenu而言,典型写法如下:
struct PMImpl{//PMImpl ="PrettyMenu Impl"
std::shared_ptr<Image> bgImage;
int imageChanges;//背景图像被改变的次数
};
class PrettyMenu{
//...
public:
//...
private:
//...
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;//目前的背景图像
//...
};//
void PrettyMenu::changeBackground(std::iostream &imgSrc)
{
using std::swap;
Lock ml(&mutex);//获得mutex的副本数据
\
std::shared_ptr<PMImpl> pNew(new PMImpl (*pImpl));
pNew->bgImage.reset(new Image (imgSrc));//修改副本
++pNew->imageChanges;
swap(pImpl,pNew);//置换数据,释放mutex
}
异常安全函数提供以下三个保证之一:
1、基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
2、强烈保证:如果异常被抛出,程序状态不改变。
3、不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。
一个软件系统要不就具备异常安全性,要不就全然否定,没有所谓的“局部异常安全系统”。
请记住:
1、异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
2、“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
3、函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解inlining的里里外外
inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。在一台内存有限的机器上,过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随这些而来的效率损失。
inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内:
class Person{
public:
//...
int age() const { return theAge;}//一个隐喻的inline申请:age被定义于class定义式内
//...
private:
int theAge;
};
这样的函数通常是成员函数。
明确声明inline函数的做法则是在其定义式前加上关键字inline。例如标准的max template(来自)往往这样实现出来:
template<typename T>//明确申请inline
inline const T& std::max(const T&a,const T&b)
{ return a<b?b:a;}//std::max之前有关键字“inline”
Inlining在大多数C++程序中是编译期行为。如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline(无论是显式或是隐式)。一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。幸运的是大多数编译器提供了一个诊断级别;如果它们无法将你要求的函数inline化,会给你一个警告信息。
有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。
例如,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。毕竟编译器没有能力提出一个指针指向并不存在的函数。
inline void f(){};//假设编译器有意愿inline“对f的调用”
void (*pf)()=f;//pf指向 f
//...
f();//这个调用将被inlined,因为它是一个正常调用。
pf();//这个调用或许不被inlined,因为它通过函数指针达成。
实际上构造函数和析构函数往往是inlining的糟糕候选人——虽然漫不经心的情况下你不会这么认为。考虑以下Derived class构造函数:
class Base{
public:
//...
private:
std::string bm1,bm2;//base 成员1和2
};
class Derived:public Base{
public:
Derived(){}//Derived构造函数是空的吗,哦,是吗?
//...
private:
std::string dm1,dm2,dm3;//derived成员1-3
};
这个构造函数看起来是inlining的绝佳候选人,因为它根本不含任何代码。但是你的眼睛可能会欺骗你。实际上,编译器为上面那个表面上看起来为空的Derived构造函数所产生的代码,相对于以下所列的代码。
Derived::Derived{ //空白Derived构造函数的观念性实现
Base::Base();//初始化“Base成分”
try {
dm1.std::string::string();//试图构造dm1
} catch (...) {//如果抛出异常
Base::~Base();//销毁base class成分,并传播该异常
throw;
}
try {
dm2.std::string::string();//试图构造dm2
} catch (...) {//如果抛出异常
dm1.std::string::~string();//销毁dm1
Base::~Base();//销毁base class成分,并传播该异常
throw;
}
try {
dm3.std::string::string();//试图构造dm2
} catch (...) {//如果抛出异常
dm2.std::string::~string();//销毁dm2
dm1.std::string::~string();//销毁dm1
Base::~Base();//销毁base class成分,并传播该异常
throw;
}
}
这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精致复杂的做法来处理异常。尽管如此,这已能准确反映Derived的空白构造函数必须提供的行为。不论编译器在其内所做的异常处理多么精致复杂,Derived构造函数至少一定会陆续调用其成员变量和base class两者的构造函数,而那些调用(它们自身也可能被inlined)会影响编译器是否对此空白函数inlining。
inline函数无法随着程序库的升级而升级。换句话说如果f是程序库内的一个inline函数,客户将“f 函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f 的客户端程序都必须重新编译。然而,如果f是non-inline函数,一旦它有任何修改,客户端只需要重新连接就好,远比重新编译的负担少很多。如果程序库采用动态连接,升级版函数甚至可以不知不觉地被应用程序吸纳。
请记住:
1、将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可以使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2、不要只因为function template出现在头文件,就将它们声明为inline。
条款31:将文件间的编译依存关系降至最低
假如定义一个Person class,一般会这样写:
#include<string>
#include"date.h"
#include"address.h"
class Person{
public:
Person(const std::string&name,const Date& birthday,
const Address& addr);
string name() const;
string birthDate() const;
string address() const;
private:
std::string theName;//实现细目
Date theBirthDate;
Address thAddres;
};
这么一来在Person定义文件和其含入文件之间形成了一种编译依存关系。
针对Person,我们可以这样做:把Person分割为两个class,一个只提供接口,另一个负责实现该接口。如果负责实现的那个所谓的implementation class取名为PersonImpl,Person将定义如下:
#include<string>
#include<memory>
class PersonImpl;//Person实现类的前置声明
class Date; //Person接口用到的class的前置声明
class Address;
class Person{
public:
Person(const std::string&name,const Date& birthday,
const Address& addr);
string name() const;
string birthDate() const;
string address() const;
//...
private:
std::shared_ptr<PersonImpl> pImpl;//指针,指向实现物;
};
在这里,main class (Person)只内含一个指针成员,指向其实现类(PersonImpl),这种设计被称为pimpl idiom(pimpl :pointer to implementation),这种class内的指针名称往往就是pImpl。
这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其它文件内的声明式(而非定义式)相依。其它每一件事都源自于这个简单的设计策略:
1、如果使用objece reference 或objece pointer可以完成任务,就不要使用object。
2、如果能够,尽量以class声明式替换class定义式。
像下面这样
class Date;//class声明式
Date today();//没问题,这里并不需要
void clearAppointments(Date d);//Date的定义式
3、为声明式和定义式提供不同的头文件
就像下面:
#include"datefwd.h"//这个头文件内声明(但未定义)class Date
Date today();//没问题,这里并不需要
void clearAppointments(Date d);//Date的定义式
下面是两个成员函数的实现:
#include"Person.h"
#include"PersonImpl.h"
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();
}
上面的Person::name函数内调用PersonImpl::name,让Person变成一个Handle class并不会改变它做的事情,只会改变它做事的方法。
另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class(抽象基类)。称为interface class。
一个针对Person而写的Interface class或许看起来像这样。
class Person{
public:
virtual ~Person();
virtual string name() const=0;
virtual string birthDate() const=0;
virtual string address() const=0;
//...
};
这个class的客户必须以Person的pointer和reference来撰写应用程序。
interface class的客户必须有办法为这种class创建新对象。他们通常调用一个特殊函数,此函数扮演“真正将被具体化”的那个derived class的构造函数角色。这样的函数通常被称为factory(工厂)函数或者virtual 构造函数。他们返回指针(或者智能指针),指向动态分配所得对象,而该对象支持interface class的接口。这样的函数又往往在interface class内被声明为static:
class Person{
public:
//...
//返回一个shared_ptr,指向一个新的Person,并以给定之参数初始化
static std::shared_ptr<Person> create(const std::string&name,const Date& birthday,const Address& addr)
//...
};
客户会这样使用它们:
string name;
Date dateOfBirth;
Address address;
//...
//创建一个对象,支持Person接口
std::shared_ptr<Person> pp(Person::create(name,dateOfBirth,address));
//...
std::cout<<pp->name() //通过Person的接口使用这个对象
<<"was born on"
<<pp->birthDate()
<<" and now lives at"
<<pp->address(); //当pp离开作用域,对象会被自动删除
当然,支持interface class接口的那个具象类(concrete class)必须被定义出来,而且真正的构造函数必须被调用。一切都在virtual构造函数实现码所在文件内秘密发生。假设interface class Person有个具象的derived class RealPerson,后者提供继承而来的virtual函数的实现。
class RealPerson:public Person{
public:
RealPerson(const std::string&name,const Date& birthday,
const Address& addr)
:theName(name),theBirthDate(birthday),thAddres(addr)
{}
virtual ~RealPerson(){}
string name() const;//这些函数的实现码并不显示于此,但他们很容易想象
string birthDate() const;
string address() const;
private:
std::string theName;//实现细目
Date theBirthDate;
Address thAddres;
};
有了RealPerson之后,写出Person::create就真的一点也不稀奇了:
std::shared_ptr<Person> Person::create(const std::string &name, const int &birthday, const int &addr)
{
return std::shared_ptr<Person> (new RealPerson(name,birthday,addr));
}
一个更现实的Person::create实现代码会创建不同的类型的derived class对象,取决于诸如额外参数值、读自文件或数据库的数据、环境变量等等。
RealPerson示范实现interface class的两个最常见机制之一:从interface class(Person)继承接口规格,然后实现出接口所覆盖的函数。interface class的第二个实现法涉及多重继承。
Handle class和Interface class 解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。
在Handle class身上,成员函数必须通过implementation pointer 取得对象数据。那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加implementation pointer 的大小。最后,implementation pointer 必须初始化(在Handle class构造函数内),指向一个动态分配得来的implementation object,所以你将蒙受内存动态分配(及其后的释放动作)而来的额外开销,以及遭遇的bad_alloc异常(内存不足)的可能性。
至于Interface class,由于每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃成本。此外Interface class派生的对象必须内含一个vptr(virtual table pointer),这个指针可能会增加存放对象所需的内存数量——实际上取决于这个对象除了Interface class之外是否还有其他virtual 函数来源。
当handle class和interface class导致速度和/或大小差异过于重大以至于class之间耦合相形之下不成为关键时,就以具象类(concrete class)替换handle class和interface class。
请记住:
1、支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle class和Interface class。
2、程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及template都适用。
来源:Effective C++
仅供学习,侵删