effective C++: 5实现

五.实现
大多数情况下,适当提出拟的类定义以及函数声明,是花费最多心力的两件事。尽管如此,还是有很多东西需要小心:太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handls)”可能会破坏封装并留给客户虚吊号码牌;为考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合则可能导致让人不满意的冗长建置时间。

条款26:尽可能延后变量定义式的出现实现时间

“尽可能延后”的意义有两层:

1.延后变量的定义,直到非得使用该变量的前一刻为止。否则在变量定义与使用之间的代码可能抛出异常,那么变量的构造和析构成本浪费。

2.应该尝试延后这份定义直到能够给他初值参数为止。这样不仅能避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。因为“通过默认构造函数构造出一个对象然后对他赋值”比“直接在构造时指定初值”效率低。

如果变量在循环内使用考虑一下情况:


//方法A:定义循环外


Widget w;


for (int i = 0; i < n; ++i) 





w = some value dependent on i;


...


}     //1个构造函数+1个析构函数+n个赋值操作;


//方法B:定义循环外


for (int i = 0; i < n; ++i) 


{


Widget w(some value dependent on i);  


...


}     //n个构造函数+n个析构函数

做法A: 1个构造函数+1个析构函数+n个赋值操作

做法B:n个构造函数+n个析构函数

此外,做法A作用域比B大,有时对程序可理解性和易维护性造成冲突。

因此,除非:1.你知道赋值成本比“构造+析构”成本低;2.你正在处理代码中效率高度敏感的部分,否则应该使用方法B。

请记住:

尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。

条款27:尽量少做转型动作

C++提供了四种新式的转型

A:const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++ style转型操作符。

B:dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某些对象是否归属于集成体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。

C:reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它并不可移植。

D:static_cast用来强迫隐式转换,例如将non-const对象转换成cosnt对象,将int转换成double,将void*指针转为typed 指针,将pointer-to-base转为pointer-to-derived等等。

新式转型比较受欢迎:1,很容易在代码中被辨别出来;2,各种转型动作愈窄化,编译器愈能诊断出错误的运用。

唯一使用旧式转型的时机是当调用一个explicit构造函数将一个对象传递给一个函数时,如下:

class Widget{ 

public: 

    explicit Widget(int size); 

    ... 

}; 

void doSomeWork(const Widget &w); 

doSomeWork(Widget(15));//函数风格的转型动作创建一个Widget 

doSomeWork(static_cast<Widget>(15));//c++风格的转型动作创建一个Widget对象

蓄意的“对象生成”动作不怎么像“转型”,很可能使用函数风格的转型动作,而不使用static_cast。但有时最好忽略你的感觉,始终理智的使用新式转型。

任何一个类型转换(不论是通过转型动作而进行的显示转换还是通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。例如在这段程序中:

int x, y; 

... 

double d = static_cast<double>(x)/y;//使用浮点数除法

将int 转换成double几乎肯定会产生一些码,因为大部分计算机体系结构中,int的底层表述不同于double的地层表述。下面这个例子可能让你稍微瞪大眼睛:

class Base{...}; 

class Derived : public Base{...}; 

Derived d; 

Base* pb = &d;     //隐喻地将derived*转换成Base*

这里我们只是建立一个base class指针指向一个derived class对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量在运行期被施行于Derived*指针身上,用于取得正确的Base*指针值。

上述例子表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(例如“以Base*指向它”时的地址和以“Derived*指向它”时的地址

)。c,java,c#不可能发生这种事,但c++可能!实际上一旦使用多重继承,这事几乎一直发生着。即使是在单一继承中也可能发生。虽然这还有其他意涵,但至少意味着你通常应该避免做出“对象在c++中如何布局”的假设,更不应该以此假设为基础执行任何转型动作。例如将对象地址转型成char*指针,然后在他们身上进行指针运算,几乎总是导致无意义(不明确)行为。

对象的布局方式和他们的地址计算方式随编译器的不同而不同,那意味着“由于知道对象如何布局”而设计的转型,在某一平台行的通,在其他平台并不一定行得通。

另一件关于转型的有趣事情是:我们很容易写出某些似是而非的代码(其他语言中也许是对的)。例如SpecialWindow的onResize被要求首先调用Window的

onResize。下面是看起来对,实际上错:

class Window{ 

public: 

    virtual void onResize(){...} 

    ... 

}; 

class SpecialWinddow:public Window{ 

public: 

    virtual void onResize(){ 

       static_cast<Window>(*this).onResize();//将*this转换成Window,然后调用其onResize;这样不行! 

        ... 

    } 

};

它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象的base class成分”的暂时副本身上的onResize!并不是在当前对象身上调用Window::onResize之后又在该对象上执行SpecialWindow专属行为。不,它是在“当前对象之base calss成分”的副本上调用Window::onResize,然后在当前对象上执行SpecialWindow专属动作。如果Window::onResize修改了对象内容,当前对象其实没被改动,改动的是副本。然而SpecialWindow::onResize内如果也修改对象,当前对象真的会被改动。这使当前对象进入一种“伤残”状态:其base class成分的更改没有落实,而derived class成分的更改倒是落实了。

解决之道是拿掉转型动作,代之你真正想要说的话。所以,真正的解决方法是:

class SpecialWinddow:public Window{ 

public: 

    virtual void onResize(){ 

        Window::onResize();//调用Window::onResize作用于*this身上 

        ... 

    } 

};

在探究dynamic_cast设计意涵之前,值得注意的是,dynamic_cast的许多实现版本执行速度相当慢。假如至少有一个很普通的实现版本基于“class名称之字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行dynamic_cast,可能会耗用多达四次的strcmp调用,用以比较class名称。深度继承或多重继承的成本更高!某些实现版本这样做有其原因(它们必须支持动态链接)。在对注重效率的代码中更应该对dynamic_cast保持机敏猜疑。之所以需要用dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上只有一个“指向base”的pointer或者reference,你只能靠他们来处理对象。两个一般性做法可以避免这个问题:

一,使用容器,并在其中存储直接指向derived class 对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要。假设先前的Window/SpecialWindow继承体系中有SpecialWindows才支持闪烁效果,试着不要这样做:

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 != vinPtrs.end(); ++iter) 



    if(SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get())) 

        psw->blink(); 

}

应该改而这样做:

typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VSPW; 

VSPW winPtrs; 

for (VSPW::iterator iter = winPtrs.begin(); iter != vinPtrs.end(); ++iter) 



    (*iter)->blink(); 

}

当然,这种做法无法在同一个容器内存储指针“指向所有可能之各种Window派生类”。如果需要处理多种窗口类型,你可能需要多个容器,他们都必须具备类型安全性(type-safe)。

另一种做法让你通过base class接口处理“所有可能之各种window派生类”,那就是在base class 里提供virtual函数做你想对各个Window派生类做的事。

虽然只有SpecialWindows可以闪烁,但或许将闪烁函数声明于base class内并提供一份什么也不做的缺省版本是有意义的:

class Window{ 

public: 

    virtual void blink(){} 

}; 

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 != vinPtrs.end(); ++iter) 



    (*iter)->blink(); 

}

无论哪一种写法,并非放之四海皆准,但在许多情况下它们都提供一个可行的dynamic_cast替代方案。当它们有此功效时,你应该欣然拥抱它们。

绝对必须拒绝的是所谓的“连串(cascading)dynamic_casts”:

typedef std::vector<std::tr1::shared_ptr<Window>> VPW; 

VPW winPtrs; 

for (VPW::iterator iter = winPtrs.begin(); iter != vinPtrs.end(); ++iter) 



    if (SpecialWindow1 * psw1 = dynamic_cast<SpecialWindow1*>(iter->get())){...} 

    else if (SpecialWindow2 * psw1 = dynamic_cast<SpecialWindow2*>(iter->get())){...} 

    else if (SpecialWindow3 * psw1 = dynamic_cast<SpecialWindow3*>(iter->get())){...} 

...

}

这样产生出来的代码又大又慢,而且基础不稳,因为每次Window class集成体系一有改变,所有这一类代码都必须再次检阅看看是否需要修改。例如一旦加入新的derived class,或许上述连串判断中需要加入新的条件分支。这样的代码应该总是以某些“基于virtual函数调用”的东西取而代之。

优良的c++代码很少使用转型,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏龌龊的动作的影响。

 

请记住:

1.如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

2.如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

3.宁可使用C++ style(新式)转型,不要使用旧式转型。前者很容易辨认出来,而且也有着比较分门别类的职责。

条款28:避免返回handle指向对象内部成分

假设程序涉及矩形。为了让Rectangle对象尽可能小,可能把定义矩形的点放在一个辅助的struct内再让Rectangle去指它:

class Point{ 

public: 

    Point(int x, int y); 

    ... 

    void setX(int newVal); 

    void setY(int newVal); 

    ... 

}; 

struct RectData{ 

    Point ulhc; 

    Point lrhc; 

}; 

class Rectangle{ 

    ... 

    Point& upperLeft()const {return pData->ulhc;} 

    Point& lowerRight()const {return pData->lrhc;} 

private: 

    std::tr1::shared_ptr<RectData> pData; 

};

这样的设计可以通过编译,但却是错误的。实际上自相矛盾,一方面upperleft和lowerRight被声明为const,不让客户修改Rectangle。另一方面,这两个函数都返回reference指向private数据,调用者可以通过这些reference更改内部数据:

Point coord1(0,0); 

Point coord2(100,100); 

const Rectangle rec(coord1, coord2); 

rec.upperLeft().setX(50);//现在rec变成从(50,0)到(100,100)

第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。

第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关,而它又被存储在对象之外,那么这个函数的调用者可以修改那笔数据。这正是bitwise constness的一个附带结果,条款3。

如果它们返回的是指针或迭代器,相同的结果还会发生,原因相同。reference、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据的handle”,随之而来的便是“降低对象封装性”的风险。同时,也可能造成“虽然调用const成员函数却造成对象状态被更改”。

通常我们认为,对象的“内部”就是指它的成员变量,其实不被公开使用的成员函数(protected或private)也是对象“内部”的一部分,所以也不该返它们的handles。否则,它们的访问级别就会提高到返回它们的成员函数的访问级别。

上述两个问题可以在它们的返回类型上加上const即可:

class Rectangle{ 

    ... 

    const Point& upperLeft()const {return pData->ulhc;} 

    const Point& lowerRight()const {return pData->lrhc;} 

private: 

    std::tr1::shared_ptr<RectData> pData; 

};

const 不在是个谎言。至于封装性,这里是蓄意放松封装,有限度的放松:只让渡读取权,涂写权是禁止的。

即使这样,返回“代表对象内部”的handles,有可能在其他场合导致dangling handles(空悬的号码牌):

这种handle所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。

class GUIObject{...}; 

const Rectangle boundingBox(const GUIObject& obj);

现在,客户可能这么使用这个函数:

GUIObject *pgo; 

... 

const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());//取得一个指针指向外框左上点对boundingBox的调用获得一个新的、暂时的Rectangle对象,这个对象没有名称,权且称它为temp。随后upperLeft作用于temp对象身上,返回reference指向temp的一个内部成分。具体指向temp的那个Point对象。但是这个语句结束之后,boundingBox的返回值,也就是我们所说的temp,将被销毁,而那间接导致temp内的Points析构。最终导致pUpperLeft指向一个不再存在的对象;变成空悬、虚吊(dangling)!

只要handle被传出去了,不管这个handle是不是const,也不论返回handle的函数是不是const。这里的唯一关键是暴露在“handle比其所指对象更长寿”的风险下。

 

请记住:

避免返回handle(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。

条款29:为“异常安全”而努力是值得的

有个class用来表现夹带背景图案的GUI菜单单,这个class用于多线程环境:

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); 

}

从异常安全性的角度看,这个函数很糟。“异常安全”有两个条件:当异常被抛出时,带有异常安全性的函数会:

1.不泄露任何资源。上述代码没有做到这一点,因为一旦“new Image(imgSrc)”导致异常,对unlock就不会执行,于是互斥器就永远被把持住了。

2.不允许数据破坏。如果“new Image(imgSrc)”抛出异常,bgImage就指向一个已被删除的对象,imageChanges也已被累加,而其实并没有新的图像被成功安装起来。

解决资源泄漏的问题很容易,

void PrettyMenu::changeBackground(std::istream& imgSrc) 



    Lock ml(&mutex);//来自条款14; 

    delete bgImage; 

    ++imageChanges; 

    bgImage = new Image(imgSrc); 

}

关于“资源管理类”如Lock,一个最棒的事情是,它们通常使函数更短。较少的代码就是较好的代码,因为出错的机会比较少。

异常安全函数(Exception-safe function)提供以下三个保证之一:

基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态恐怕不可预料。如上例changeBackground使得一旦有异常被抛出时,PrettyMenu对象可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,它们恐怕必须调用某个成员函数以得知当时的背景图像是什么。

强烈保证:如果异常被抛出, 程序状态不改变。如果函数成功,就是完全成功,否则,程序会回复到“调用函数之前”的状态。

不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(如ints,指针等等)上的所有操作都提供nothrow保证。

异常安全码(Exception-safe code)必须提供上述三种保证之一。否则,它就不具备异常安全性。

一般而言,应该会想提供可实施的最强烈保证。nothrow函数很棒,但我们很难再c part of c++领域中完全没有调用任何一个可能抛出异常的函数。所以大部分函数而言,抉择往往落在基本保证和强烈保证之间对changeBackground而言,首先,从一个类型为Image*的内置指针改为一个“用于资源管理”的智能指针,第二,重新排列changeBackground内的语句次序,使得在更换图像之后再累加imageChanges。

class PrettyMenu{ 

    ... 

    std::tr1::shared_ptr<Image> bgImage; 

    ... 

};

void PrettyMenu::changeBackground(std::istream& imgSrc) 



    Lock ml(&mutex); 

    bgImage.reset(new Image(imgSrc)); 

    ++imageChanges; 

}

不再需要手动delete旧图像,只有在reset在其参数(也就是“new Image(imgSrc)”的执行结果)被成功生成之后才会被调用。美中不足的是参数imgSrc。如果Image构造函数抛出异常,有可能输入流的读取记号(read marker)已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。所以在解决这个之前只提供基本点异常安全保证。

有一个一般化的策略很典型会导致强烈保证,被称为“copy and swap”:为打算修改的对象做一个副本,在那个副本上做一切必要修改。若有任何修改动作抛出异常,源对象仍然保持未改变状态。待所有改变都成功后,再将修改过的副本和原对象在一个不抛出异常的swap中置换

实现上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予源对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。对PrettyMenu而言,典型的写法如下:

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);//置换数据 

}

copy and swap策略虽然做出“全有或全无”改变的一个好办法,但一般而言并不保证整个函数有强烈的异常安全性。

如someFunc。使用copy-and-swap策略,但函数还包括对另外连个函数f1和f2的调用:

void somefunc()

{

    …

    f1();

    f2();

    …

}

显然,如果f1或f2的异常安全性比“强烈保证”低,就很难让someFunc成为“强烈异常安全”。如果f1和f2都是“强烈异常安全”,情况并不因此好转。毕竟,如果f1圆满结束,程序状态在任何方面都有可能有所改变,因此如果f2随后抛出异常,程序状态和someFunc被调用前并不相同,甚至当f2没有改变任何东西时也是如此。

问题出现在“连带影响”,如果由函数只操作局部状态,便相对容易的提供强烈保证,但是函数对“非局部性数据”有连带影响时,提供强烈保证就困难的多。例如,如果调用f1带来的影响是某个数据库被改动了,那就很难让someFunc具备强烈安全性。另一个主题是效率。copy-and-swap得好用你可能无法(或不愿意)供应的时间和空间。所以,“强烈保证”并不是在任何时候都显得实际。

当“强烈保证”不切实际时,你就必须提供“基本保证”。

你应该挑选“现实可操作”条件下最强烈等级,只有当你的函数调用了传统代码,才别无选择的将它设为“无任何保证”。

 

请记住:

1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

2.“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。

3.函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款30:透彻了解inlining的里里外外

Inline函数,多棒的点子!它们看起来像函数,动作像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。你实际获得的比想象的还多,编译器有能力对执行语境相关最优化。然而编写程序就像现实生活一样,没有白吃的午餐。inline函数也不例外,这样做可能增加你的目标码。

如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。

记住,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内,这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline。明确声明inline函数的做法则是在其定义式钱加上关键字inline。

Inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。

Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道哦啊它长什么样子。

Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明inline;但如果你写的template煤油理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。

一个表面上看似inline的函数是否真实inline,取决于你的建置环境,主要取决于编译器。

有的时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体(函数指针,构造函数,析构函数)。

对程序开发而言,将上述所有考虑牢记在新很是重要,但若从纯粹实用观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策。

这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。

     

请记住:

将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。

不要只因为function templates出现在头文件,就将它们声明为inline。

条款31:将文件间的编译依存关系降至最低

请记住:

支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。

程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。

上一篇:取得网站的IP 地址


下一篇:IE (6-11)版本,在使用iframe的框架时,通过a标签javascript:; 和js跳转parent.location的时候 出现在新页面打开的情况