5. 实现 摘录

适当提出你的classes(class template)定义以及function(function template)声明,是花费最多心力的两件事。一旦正确完成它们,相应的实现大多直截了当。

太快定义变量可能造成效率上的拖延。

过度使用转型cast可能导致代码变慢又难维护,又招来微妙理解的错误;

返回对象“内部数据之号码牌handle”可能会导致封装并留给客户danging handles;

未考虑异常带来的冲击可能导致资源泄露和数据败坏;

过热热心inlining可能导致代码膨胀;

过度耦合coupling则可能让人不满意冗长build time。

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

std::string encryptPassword(const std::string& password)
{
    ...
    encrtpt(password);
    return encryted;
}

你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。

“但循环怎办?”

// A方案
Widget A;
for (int i = 0; i < n; i++)
{
    w = ...;
}


// B方案
for (int i = 0; i < n; i++)
{
    Widget A = ...;
}

如果class的一个赋值成本低于一组构造+析构函数,做法A大体而言比较高效。但是做法A造成名称w的作用域比做法B要大,有时那对程序的可理解性和易维护性造成冲突。因此除非(1) 你知道赋值成本比“构造 + 析构”成本低,(2)你正在处理代码中效率高度敏感(performance-sensitive),否则你应该选B。

请记住:

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

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

C++规则的设计目标之一是,保证“类型错误”绝不可能发生。

不幸的是,转型cast破坏了类型系统。

三种不同的转型语法,

        (T)expression;

        T(expression);

        ?_cast<T>(expression);

C++的四种新式转型:

        const_cast通常用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++转型操作符。

        dynamice_cast主要用来执行安全向下转型(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。

        reinterpret_cast意图执行低级转型,实际动作(结果)可能取决于编译器,这也就代表它不可移植。

        static_cast用来强迫隐式转换(implicit conversion)。它可以用来执行上述多种转换的反转换。

旧式转型仍然合法,但新式转型较受欢迎。原因在于:第一,它们很容易在代码中被辨识出来。第二,各转型动作的目标越窄,编译器越可能诊断出错误的运用。

许多程序员相信,转型其实上面都没做,只是告诉编译器把某种类型视为另一种类型,这是错误观念。任何一个类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的代码。

单一对象(子类)可能拥有一个以上的地址(静态类型,动态类型),而且一旦发生多重继承,此事会一直存在。

有时候需要一个偏移量。对象的布局方式和它们的地址计算方式随着编译器的不同而不同,那意味着由于知道对象如何布局而设计的转型,在某一个平台行得通,在其他平台并不一定行得通。

另一个关于转型有趣的事情是:我们很容易写出一些似是而非的代码:

class Window
{
public:
    virtual void onResize() {...}
    ...
};

class SpecialWindow : public Window
{
public:
    virtual void OnResize ()
    {
        static_cast<Window>(*this).onResize(); // 奇怪的代码
    }

};


// 修改为
class SpecialWindow : public Window
{
public:
    virtual void OnResize ()
    {
        Window::onResize(); 
    }

};

上述代码并非在当前对象身上调用Window.onResize之后,又在该对象身上执行SpecialWindow专属动作,不。它是在“当前对象之base class成分”的副本上调用Window::onResize,然后在当前对象身上执行SpecialWindow专属动作。如果Window::onResize修改了当前对象内容,当前对象其实没被改动,改动的是副本。然而SpecialWindow::onResize内如果也修改对象,当前对象真的会被改动。这使当前对象进入一种“伤残”状态:其base class成分的更改没有落实,而derived class成分的更改倒是落实了。

之所以需要有dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你手上却只有一个“指向base”的pointer或reference,你只能靠转型来处理对象。有两个一般性做法可以避免这个问题:

        第一,使用容器并在其中存储直接指向derived class对象的指针。

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<Window>> VPW;
VPW winPtrs;
..
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
    (*iter)->blink();
}

                当然了,这种做法你无法在同一个容器内存储指针“指向所有可能值各种Window派生类”。如果真要处理多个窗口类型,你可能需要多个容器。

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

绝对要避免的一件事情是所谓的连串(cascading)dynamic_cast,也就是看起来基类到深处派生类。

就像面对众多蹊跷的构造函数一样,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏动作影响。

请记住:

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

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

        宁可使用C++ style转型,不要使用旧式转型。前者容易识别,而且也比较有着分门别类的职能。

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

class Point
{
public:
    Point(int x, int y);
    ...
    void setY(int newVal);
    void sety(int newVal);
    ...
};

struct RectData
{
    Point ulhc;    // ulhc = "upper left-hand corner"
    Point lrhc;    // lrhc = "low right-hand corner"
};

class Rectangle
{
public:
    ...
    Point& upperLeft() const {  return pData->ulhc; }
    Point& lowerRight() const {  return pData->lrhc; }
private:
    std::tr1::shared_ptr<RectData> pData;
};

这种设计可通过编译,但确实错误的。实际上它是自我矛盾的。一方面upperLeft和lowerRight被声明为const成员函数,但都返回reference指向private内部数据。

这给我们带来两个教训。第一,成员变量的封装性最多只等于“返回其reference”的函数的访问权限。第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。

通常我们认为,对象的“内部”就是指它的成员变量,但其实不被公开使用的成员函数(也就是被声明为protected或private)也是对象“内部”的一部分。因此也应该留心不要返回它们的handles。

我们在这些函数身上遭遇的两个问题可以轻松去除,只要对它们的返回类型加上const即可。

class Rectangle
{
public:
    ...
    const Point& upperLeft() const {  return pData->ulhc; }
    const Point& lowerRight() const {  return pData->lrhc; }
    ...
};

函数返回值很有可能造成handle所指的东西不复存在,造成dangling handles。不论这个handle是个指针或迭代器或reference,也不论这个handle是否为const,也不论返回handle的成员函数是否为reference。这里唯一的关键是:有个handle被传出去了,一旦如此你就暴露在“handle比其所指对象更长寿”的风险下。

请记住:

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

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

当异常被抛出时,带有异常安全性的函数会:

        不泄露任何资源;

        不允许数据败坏;

解决资源泄露的问题很容易,以对象管理资源。

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

        基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。

        强烈保证:如果异常被抛出,程序状态不改变。

        不抛掷(nothorw)状态:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能--作用于内置类型身上的所有操作都挺nothrow保证。这是异常安全码中必不可少的关键基础材料。

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

实际上通常是将所有隶属对象的数据从原对象放进另一个对象内,然后赋予原对象一个指,指向那个所谓的实现对象(implementation onject)。

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 m1(&mutex);                                       // 获得mutex的副本数据
    std::tr1::shread_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new mage(imgSrc));                 // 修改副本
    ++pNew->imageChanges;
    swap(pImpl, pNew);                                     // 交换数据,释放mutex
}

此例选择让PMImpl成为一个struct而不是class,这是因为PrettyMenu的数据封装性已经由pImpl的private获得了保证。

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

如果系统内有一个函数函数不具备异常安全性,整个系统就不具备异常安全性。

请记住:

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

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

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

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

inline函数,它们看起来像函数,动作像函数,比宏好得多,可以调用它们又不需要蒙受函数调用所招致的额外开销。

编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,所以当你inline某个函数,或许编译器就因此有能力对它(函数本体)执行语境相关最优化。

inling函数背后的整体观念是:将“对此函数”的每一个调用“都以函数本体替换之”。

这样做可能会增加你的目标码大小。即使拥有虚内存,inline造成的代码膨胀会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随这些而来的效率损失。

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

隐喻方式是将函数定义与class定义式内;

class Person
{
public:
    ...
    int age() const { return theAge; } // 一个隐喻的inling申请
    ...
private:
    int theAge;
};
template<yepname T>
inline
const T& std::max(const T& a, const T& b)
{ return a < b ? b : a; }

我们发现inline函数和template两者通常都被定义与头文件内。这使得某些程序员认为function template一定必须是inline。这个结论不但无效而且有害。

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

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

template的具现化与inling无关。

inlining需要成本,你不会想在没有事先考虑的情况下就招来那些成本吧。

大部分编译器拒绝将太过复杂的(例如带有递归或循环)的函数inlining,而所有对virtual函数的调用也都会使inlinig落空。因为virtual意味着等待,直到运行时才确定调用哪个函数,而inline意味着执行前,先将调用动作替换为被调用函数的本体。

编译器通常不对函数指针而进行的调用inlining。

实际上构造函数和析构函数往往是inlining的糟糕候选人。

class Base
{
public:
    ...
private:
    std::string bm1, bm2;
};

class Derived : public Base
{
public:
    Derived() {}
private:
    std::string dm1, dm2, dm3;
};

这个构造函数看起来是inling的绝佳候选人,因为它根本不含任何代码。但是你的眼睛可能会欺骗你。

编译器为稍早的那个表面上看起来为空的Derived构造函数产生的代码,相当于:

Derived::Derived(0
{
    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构造函数至少一定会陆续调用其成员变量和base class两者的构造函数,而那些会调用(它们自身也可能被inlined)会影响编译器是否对此空白函数inlining。

程序库设计者必须评估“将函数声明为inling”的冲击:inline函数无法随着程序库的升级而升级。换句话说,如果f是程序库的一个inline函数,客户将f函数本体编译进气程序中,一旦程序设计者改变f,所有用到f的客户端程序都必须重新编译。反之,如果是non-inline,远比重新编译的负担少很多。如果程序库采取动态链接,升级版函数甚至可以不知不觉地被应用程序吸纳。

还有一个因素是:大部分调试器面对inline函数都束手无策。

在决定哪些函数是否声明为inline的策略是:一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。慎重使用inline便是对日后使用测试调试器带来帮助。

请记住:

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

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

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

C++并没有“将接口从实现中分离”这事做的很好。Class定义式不只详细叙述了class接口,还包括十足的实现细目。例如:

#include<string>
#include "date.h"
#include "address.h"

class Person
{
public:
    Person(const std::string& name, const Data& birthData, const Address& address);
    std::string name() const;
    std::string birthData() const;    
    std::string address() const;
    ...
private:
    std::string theName;
    Data theBirthData;
    Address theAddress;
};

不幸的是,Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。如果这些头文件中有任何一个被改变了,这些头文件所依赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。这样的连串的编译依存会对项目造成难以形容的灾难。

你或许会奇怪,为什么C++坚持将class的实现细目置于定义式中?为什么不这样定义Person,将实现细目分开叙述?

namespace std
{
    class string;
}

// 前置声明
class Date;
class Address;
class Person
{
public:
    Person(const std::string& name, const Data& birthData, const Address& address);
    std::string name() const;
    std::string birthData() const;    
    std::string address() const;
    ...
};

上述代码的前置声明想法有两个问题。第一,string不是个class,它是个typedef。如果解析parse标准头文件是个问题,你可能需要改变你的接口设计,避免使用标准库中“引发不收欢迎值#inclulde”。第二困难是,编译器必须在编译时候知道对象的大小。

int main()
{
    int x;
    Person p(params);
}

将Person分隔为两个classes,一个只提供接口,另一个负责实现该接口。

#include<string>
#include<memory>

// 前置声明
class Date;
class Address;
class PersonImpl;

class Person
{
public:
    Person(const std::string& name, const Data& birthData, const Address& address);
    std::string name() const;
    std::string birthData() const;    
    std::string address() const;
    ...
private:
    std::tr1:shared_ptr<PersonImpl> pImpl;
};

这般设计常被称为pimpl idiom。这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译器依存性最小化的本质:现实中总让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式相依。

如果使用object reference或onject pointer可以完成任务,就不要使用objcts。

尽量能够以class声明式替换class定义式。

如果能够将“提供class定义式”(通过#include完成)的义务从“函数声明所在”之头文件转移到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性移除。

为声明式和定义式提供不同的头文件。因此程序库客户应该总是#include一个声明文件而非前置声明若干函数,程序库作者也应该提供这两个头文件。

C++标准程序库中<iosfwd>深居启发意义的另一个原因是,它分外彰显“本条款”适用于template与non-template。

C++也提供关键字export,允许将template声明式和template定义式分割于不同的文件内。

#include "Person.h"
#include "PersonImpl.h"

Person::Person(const std::string& name, const Data& birthData, const Address& address)
:pImpl(new PersonImpl(name, birthdayData, address))
{}

std::string Person::name() const
{
    return pImpl->name();
}

请注意,Person构造函数以new调用PersonImpl构造函数,以及Person::name函数内调用PersonImpl::name。让Person变成一个Handle并不会改变它做的事,只是会改变它做事的方式。

另一个制作Handle class的办法是,令Person成为一个特殊的abstract class,称为interface class。这种class的目的是详细--描述derived class的接口,因此它通常不带有成员变量,也没有构造函数,只有一个虚析构函数以及一组pure virtual函数,用来描述整个接口。

// interface class Person
class Person
{
public:
    virtual ~Person()
    virtual std::string name() const = 0;
    virtual std::string birthData() const = 0;    
    virtual std::string address() const = 0;
    ...
};

这个class的客户必须以Person的reference和pointer来撰写应用程序,因为它不可能针对“内含pure virtual函数的”Person class具现出实体。

Interface class的客户必须有办法为这种class创建新对象。它们通常调用一个特殊函数,此函数必须扮演“真正将被具现化”的那个derived classes的构造函数角色。这样的函数被称为factory函数或virtual构造函数。它们返回指针,指向动态分配所得对象,而该对象支持Interface class接口。这样的函数又往往在Interface class内被声明为static。

class Person
{
public:
    static std:tr1::sahred_ptr<Person> create (const std::string& name, const Dates& birthday, cont Address& addr);
    ...
};


std::string name;
Dates birthday;
Address addr;

// 客户使用案例
std:tr1::sahred_ptr<Person> pp(Person::create(name, birthday, addr));

当然了,支持interface class接口的那个具象类(concrete class)必须被定义出来,而且真正的构造函数必须被调用。一切都在virtual构造函数实现码所在的文件内发生。

class RealPerson : public Person
{
public:
    RealPerson(const std::string& name, const Dates& birthday, cont Address& addr)
    : theName(name), theBirthday(birthday), theAddress(addr) {}

    virtual ~RealPerson() {}
    std::string name() const;
    std::string birthData() const;    
    std::string address() const;
private:
    std::string theName;
    Data theBirthday;
    Address theAddress;
};

std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Dates& birthday, cont Address& addr)
{
    return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr))
}

RealPerson示范实现Interface class的两个最常见机制之一:从Interface class(Person)继承接口规格,然后实现出接口所覆盖的函数。Interface class的第二个实现设计多重继承。

Handle class和Interface class解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。代价则是:它使你在运行期间丧失若干速度,又让你为每个对象超额付出若干内存。

在Handle class身上,成员函数必须通过implementation pointer取得对象数据。那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加implementation pointer的大小。最后,implementation pointer必须初始化,指向一个动态分配来的implementation object,所以你将受动态内存分配及其释放而来的额外开销,以及遭遇bad_alloc异常(内存不足)的可能性。

至于Interface class,由于每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃成本。此外Interface class派生的对象必须内含一个vptr,这个指针可能会增加存放对象所需要的的内存数量--实际取决于这个对象处理Interface class之外是否还有其他virtual函数来源。

不论handle class和interface class,一旦脱离了inline函数都无法有太大作为。但handle 和interface class正是特别被设计用来隐藏实现细节的函数本体。

当handle class和interface class导致速度或大小差异过于重大,以至于classes之间的耦合相比之下不成为关键时,就以具象类替换handle class和interface class。

请记住:

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

        程序库文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及template都适用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值