Effective C++笔记(4)

        五、实现

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

       只要你定义一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流(control flow)到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受其析构成本。

        std::string encryptPassword(const std::string& password)

        {

               using namespace std;

               string encrypted;

               if(password.length() < MinimumPasswordLength)

               {

                     throw logic_error("Password is too short");

               }

               ...

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

               ...

                return encrypted;

        }

         为了提高效率可以使用如下做法:
         

         std::string encryptPassword(const std::string& password)

        {

              ...                                           //检查length,如前

              std::string encrypted;            //default-construct encrypted

              encrypted = password;         //赋值给encrypted

             encrypt(encrypted);

             return encrypted;

        }

         更受欢迎的做法是跳过毫无意义的default构造过程:         

         std::string encryptPassword(const std::string& password)

        {

              ...                                           //检查length,如前

              std::string encrypted(password);            //通过copy构造函数定义并初始化       

             encrypt(encrypted);

             return encrypted;

        }

        你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。更深一层说,以“具明显意义之初值”将变量初始化,还可以附带说明变量的目的。

        对应循环如何处理:

        //方法A:定义于循环外                                              //方法B:定义于循环外

        Widget w;                                                   

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

        {                                                                                  {

            w = 取决于i的某个值;                                                 Widget w(取决于i的某个值);

             ...                                                                                  ...

        }                                                                                   }

       在Widget函数内部,以上两种写法的成本如下:

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

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

       除非你知道赋值成本比“析构+构造”成本低,你处理代码中效率高度敏感(performance-sensitive)的部分,否则你应该使用做法B。

       请记住:

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

     

       

      (27):尽量少做转型动作

        (T)expression和T(expression)为两种“旧式转型(old-style casts)”。

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

             ...

        };

        void doSomeWork(const Widget& w);

        doSomeWork(Widget(15));                     //以一个int加上“函数风格”的转型动作创建一个Widget。

        doSomeWork(static_cast<Widget>(15)) //以一个int加上“C++风格”的转型动作创建一个Widget。

        

        任何一个类型转换(不论是通过转型操作而进行的显示转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运气期间执行的码。

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

       另一个关于转型的有趣事情是:我们容易写出某些似是而非的代码(在其他语言中也许真是对的)。

       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!(译注:函数就是函数,成员函数只有一份,“调用起哪个对象身上的函数”有什么关系呢?关键在于成员函数都有个隐藏的this指针,会因此影响成员函数操作的数据。) 再说一次,上述代码并非在当前对象身上调用Window::onResize之后又在该对象身上执行SpecialWindow专属动作。不,它是在“当前对象之base class成分”的副本上调用Window::onResize,然后执行SpecialWindow专属动作。如果Window::onResize修改了对象内容(不能说没有可能性,因为onResize是个non-const成员函数),当前对象其实没被改动,改动的是副本。然而SpecialWindow::onResize内如果也修改对象,当前对象真的会被改动。这使当前对象进入一种“伤残”状态:其base class成分的更改没有落实,而derived class成分的更改到时落实了。

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

     class SpecialWindow:public Window {

     public:

        virtual void onResize() {

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

         ...

        }

    }                 

  

    dynamic_cast的许多实现版本指向速度相当慢。之所以需要dynamic_cast,通常是因为你想在一个你认定为derived class对象身上指向derived class操作函数,但你的手上却只有一个“指向base”的pointer或reference,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题。

     第一,使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要。

           class Window { ... };

           class SpecialWindow:public Window {

           public:

                void blink();

                ...

          };

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

          VPM winPtrs;

          ...

          for(VPM::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>> VPSM;

          VPSM winPtrs;

          ...

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

           {               

                      (*iter)->blink();              //这样写比较好,不用dynamic_cast

          }

         另一种如下:

           class Window { 

           public:

                virtual void blink() {}

                  ... 

          };

           class SpecialWindow:public Window {

           public:

               void void blink() { ... };

                ...

          };

           class SpecialWindow:public Window {

           public:

                void blink();

                ...

          };         

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

          VPSM winPtrs;

          ...

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

           {               

                      (*iter)->blink();              //这样写比较好,不用dynamic_cast

          }

          不论哪一种写法——“使用类型安全器”或“将virtual函数往继承体系上方移动”——都并非放之四海而皆准,但在许多情况下它们都提供一个可行的dynamic_cast替代方案。

          绝对必须避免的一件事是所谓的“连串(cascading)dynamic_casts”。

           请记住:

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

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

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


        

       (28):避免返回handles指向对象内部成分

        class Point {

         public:

             Point(int x,int y);

             ...

             void setX(int newVal);

             void setY(int newVal);

         };

         strcut RectData {        //这些“点”数据用来表现一个矩形

          Point ulhc;                 //ulhc="upper left-hand coner"(左上角)

          Point lrhc;                 //lrhc="lower right-hand coner"(右下角)

        };

        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成员函数,因为它们的目的只是为了提供客户一个得知Rectangle相关坐标点的方法,而不是让客户修改Rectangle。另一方面两个函数却都返回reference指向private内部数据,调用者于是可通过这些reference更改内部数据。

      这立刻带给我们两个教训:成员变量的封装性最多只等于“返回其reference”的函数的访问级别。本例之中虽然ulhc和lrhc都被声明为private,它们实际上却是public,因为public函数upperLeft和lowerRight传出了它们的reference。如果成员函数传出一个reference,后者所指数据与对象自身有关联,而它有被存储于对象之外,那么这个函数的调用者可以修改那笔数据。

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

       “内部”不仅指成员变量,其实不被公开使用的成员函数也算。

       通过在返回类型加上const即解决问题:

       class Rectangle {

       public:

          ...

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

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

          ...

      };

      上面的函数仍然返回了“代表对象内部”的handle,有可能在某些场合带来问题。更明确说,它可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。

        class GUIObject { ... };

        class Rectangle boundingBox(const GUIObject& obj);  //以by value方式返回一个矩形

       调用如下:

        GUIObject* pgo;

        ...

        const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft);

        一旦产出pUpperLeft的那个语句结束,pUpperLeft也就变成空悬、虚吊(dangling)!

        请记住:

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


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

          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);                                    //释放互斥器

             }

             从“异常安全性”的观点来看,上面的函数很糟。

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

             不泄露任何资源。   不允许数据败坏。

             使用资源管理类Lock很容易的防止资源泄露:              

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

            {

                 Lock ml(&mutex);

                 delete bgImage;                                    //摆脱旧的背景图像

                 ++imageChanges;                                //修改图像变更次数

                bgImage = new Image(imgSrc);             //安装新的背景图像              

             }

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

             基本承诺:如果异常抛出,程序内的任何事物仍然保持在有效状态下。

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

             nothrow保证:承诺不抛出异常,因为它们总是能够完成原先承诺的功能。

            

            不要为了表示某件事情发生而改变对象状态,除非那件事情真的发生了。

            class PrettyMenu {

             ...

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

             ...

           }

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

          {

                 Lock  ml(&mutex);

                 bgImage.reset(new Image(imgSrc));   //以“new Image”的执行结果设定bgImage内部指针

                 ++imageChanges;

         }

         copy and swap策略可以导致强烈保证。

         实现上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种手法常被称为pimpl idiom。

          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(pIml,pNew);

       }

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

       void someFunc()

       {

         ...

         f1();

         f2();

         ...

       }

       如果f1()或f2(0的异常安全性比“强烈保证”低,就很难让someFunc成为“强烈异常安全”。

        “强烈保证”需要保留一个对象的副本,有时不切实际,你就必须提供“基本保证”。

         请记住:

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

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

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



        (30):透视了解inlining的里里外外

         inline函数可以“免除函数调用成本”。inline函数将“对此函数的每一个调用”都有函数体替换之,可能增加你的目标码(object code)大小。

        inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。

       class Person {

        ...

        int age() const { return theAge;  }  //一个隐喻的inline申请:age被定义于class定义式内。

       ...

       private:

       int theAge;

       }

       这样的函数通常是成员函数,有时friend函数也可以,只有定义于class内。

       明确申请inline如下:

       template<typename T>

       inline const T& std::max(const T& a,const T& b)

       { return a < b ? b : a; }

        

       大部分编译器拒绝太过复杂(例如带有循环或递归)的函数inlining,而所有对virtual函数的调用(除非最平淡无奇)也会使inling落空。virtual意味“等待,直到运行期才确定调用哪个函数”,而inline意味“执行前,先调用动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,你就很难责备他们拒绝将函数本体inlining。

        这些叙述整合起来的意思就是:一个表面上看似inline的函数是否真inline,取决于你的建置环境,主要取决于编译器。

       编译器通常不对“通过函数指针而进行的调用”实施inlining,这以为这inline函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式: 

        inline void f() { ... }

        void (*pf)()  = f;

        ...

        f();              //这个调用将被inlined,因为它是一个正常调用。

        pf();            //这个调用或许不被inlined,因为它通过函数指针完成。

        

        实际上构造函数和析构函数往往是inlining的糟糕候选人。C++对于“对象被创建和被销毁时发生什么事”做了各式各样的保证。当你使用new,动态创建的对象被其构造函数自动初始化;当你使用delete,对应的析构函数会被调用。当你创建一个对象,其每一个base calss及每一个成员变量都会被自动构造;当你销毁一个对象,反向程序的析构行为亦会自动发生。异常抛出时,也有相应的处理程序。

       Derived::Derived()      //“空白Derived构造函数”的观念性实现

       {

              Base::Base();                //初始化“Base成分”

              try { dml.std::string::string(); }  //试图构造dml。

             catch (...) {                               //如果抛出异常就销毁base class成分,并传播该异常

                    Base::~Base();

                    throw;

               }

              

              try { dm2.std::string::string(); }  //试图构造dm2。

             catch (...) {                               //如果抛出异常就销毁dml,销毁base class成分,并传播该异常

                    dml.std::string::~string(); 

                    Base::~Base();

                    throw;

               }

               ...

      }

      这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精致复杂的做法来处理异常。

    

       inline函数无法随着程序库的升级而升级。

       请记住:

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

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


     

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

         对某个C++类文件进行轻微修改就要重新编译,问题出在C++并没有“将接口从实现中分离”这事做的很好。Class的定义式不只详细叙述了class接口,还包括十足的实现细目。

          class Person {

          public:

                Person(const std::string& name,cosnt Date& birthday,const Address& addr);

                std::string name() const;

                std::string birthDate() const;

                std::string address() const;

                ...

          private:

              std::string theName;      //实现细目

              Date theBirthDate;        //实现细目

              Address theAddress;    //实现细目

          }

         上面的Person类不能通过编译,需要#include<string>  #include "date.h"  #include "address.h",这样一来便是在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。如果任何一个改变,Person将重新编译。

   namespace std {

         class string;

   }

   class Date;

   class Address;         

  class Person {

          public:

                Person(const std::string& name,cosnt Date& birthday,const Address& addr);

                std::string name() const;

                std::string birthDate() const;

                std::string address() const;

                ...    

          }

         如果这样做Person的客户就只需要在Person接口被修改时才重新编译。

         存在两个问题:第一,string不是个class,它是个typedef(定义为basic_string<char>)。第二,当编译器看到Person定义式时,它也知道必须分配足够空间以放置一个Person。编译器获得这项信息的唯一办法是询问class的定义式。

         将一个指针指向Person就可以解决问题二。把Person分割为两个classes,一个只提供接口,另一个负责接口实现。

        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,cosnt 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只内含一个指针成员(这里使用tr1::shared_ptr),指向实现类(PersonImpl)。这般设计常被称为pimpl idiom(pimpl是“pointer to implementation”的缩写)。这样便实现了“接口与实现分离”。这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件的声明式(而非定义式)相依。其他每一件事都源自这个简单的设计与策略:

         如果使用object references或object pointers可以完成任务,就不要使用objects。

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

         为声明式和定义式提供不同的头文件:

         为了促进严守上述准则,需要两个头文件,一个拥有声明式,一个用于定义式。当然,这些文件必须保持一致性,如果有个声明式被改变了,两个文件都得改变。

 

         像Person这样使用pimpl idiom的classes,往往被称为Handle classes。

         #include "Person.h"                  //我们正在实现Person class,所以必须#include其class定义式(声明式?)

         #include "PersonImpl.h"           //我们也必须#include PersonImpl的class定义式,否则无法调用其成员函数;注                                                            //意,PersonImpl有着和Person完全相同的成员函数,两者接口完全相同。         

         class Person::Person(const std::string& name,cosnt Date& birthday,const Address& addr)

                     :pImpl(new PersonImpl(name,birthday,addr))

                  {}

               std::string Person::name() const

               {

                      return pImpl->name();

               }

              另一个制作handle class的办法是,令Person成为一种特殊的abstract base class(抽象基类),称为Interface class。Interface class的两个最常见机制之一:从Interface class继承接口规格,然后实现出接口所覆盖的函数。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;

                   ...

               }

             调用方法(factoy(工厂)函数):

              static :

                   class Person {

                   public:

                       ...

                       static std::tr1::shared_ptr<Person>     //返回一个tr1::shared_ptr,指向一个新的Person,并以给定之参

                            create(const std::string& name,cosnt Date& birthday,const Address& addr);  //数初始化

                      ...

                   }

                  客户会这样使用它们:

                  std::string name;

                  Date dateOfBirth;

                  Address address;

                  ...

                 //创建一个对象,支持Person接口

                 std::tr1::shared_ptr<Person> pp(Person::create(name,dayeOfBirth,address));

                  ...

                 std::cout << pp->name()  //通过Person的接口使用这个对象

                               << " was born on "

                               << pp->birthDate()

                               << " and now lives at "

                               << pp->address();

                    ...                                         //当pp离开作用域,对象会被自动删除。

                 Interface class Person有个具象的derived class RealPerson,后者提供继承而来的virtual函数的实现:

                 class RealPerson : public Person {

                 public:

                      RealPerson(const std::string& name,cosnt Date& birthday,const Address& addr)

                       : theName(name),theBirthDate(birthday),theAddress(addr)

                     {  }

                     virtual ~RealPerson() {}

                    std::string name() const;

                    std::string birthDate() const;

                    std::string address() const;

                  private:

                    std::string theName;

                    Date  theBirthDate;

                    Address theAddress;

                 }

                 有了RealPerson之后,写出Person::create就真的一点不稀奇了:

                 std::tr1::shared_ptr<Person>::create(const std::string& name,cosnt Date& birthday,const Address& addr)

                 {

                          return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr));

                 }

                 Handle classes和Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies)。

                请记住:

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值