• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            洛譯小筑

            別來無恙,我的老友…
            隨筆 - 45, 文章 - 0, 評論 - 172, 引用 - 0
            數(shù)據(jù)加載中……

            [ECPP讀書筆記 條目35] 虛函數(shù)的替代方案

            假設(shè)你正在設(shè)計一款游戲軟件,你需要為游戲中的各種角色設(shè)計一個層次結(jié)構(gòu)。你游戲的劇情中充滿了風(fēng)險刺激,那么游戲中的角色就會經(jīng)常遇到受傷或者生命值降低的情況。于是你決定為角色類提供一個成員函數(shù):healthValue,這一函數(shù)通過返回一個整數(shù)值來表示角色當(dāng)前的生命值。由于計算不同角色生命值的方式可能會不一樣,因此不妨將healthValue聲明為虛函數(shù),這樣做十分順理成章:

            class GameCharacter {

            public:

              virtual int healthValue() const; // 返回角色的生命值

              ...                              // 派生類可以重定義該函數(shù)

            };

            此處,healthValue函數(shù)并沒有直接聲明為純虛函數(shù),這一事實告訴我們計算生命值存在著一種默認(rèn)的算法(參見條目34)。

            事實上,所謂“順理成章”的方法,在某些情況下卻會成為工作的絆腳石。由于這一設(shè)計太過“順理成章”了,你也許就不會再為尋找替代方案花費足夠的精力。為了讓你從面向?qū)ο笤O(shè)計的思維定勢中解脫出來,讓我們另辟蹊徑,解決這一問題。

            模板方法模式:通過“非虛接口慣用法”實現(xiàn)

            我們從討論一個有趣的思想流派開始,這一流派堅持虛函數(shù)必定為私有函數(shù)。其追捧者也許會提出,更優(yōu)秀的方案中healthValue仍為公共成員函數(shù),但將其聲明為非虛函數(shù),并讓其調(diào)用一個私有的虛函數(shù),真正的工作由這個虛函數(shù)來完成。不妨將其命名為doHealthValue

            class GameCharacter {

            public:

              int healthValue() const            // 派生類不能對其進(jìn)行重定義

              {                                   // 參見條目36

                ...                               // 準(zhǔn)備工作:參見下文

                int retVal = doHealthValue();     // 進(jìn)行實際的工作

                ...                               // 后期工作:參見下文 

                return retVal;

              }

              ...

             

            private:

              virtual int doHealthValue() const  // 派生類可以對其進(jìn)行重定義

              {

                ...                               // 計算角色生命值的默認(rèn)算法

              }

            };

            上文的代碼中(以及本條目中所有其它代碼中),我將成員函數(shù)的定義內(nèi)容放置在了類定義中。如同條目30中所解釋的,這樣做使得這些函數(shù)隱式帶有內(nèi)聯(lián)屬性。我這樣做只是為了讓所說明的問題更加簡單明了。是否選擇內(nèi)聯(lián)與本條款的設(shè)計方案無關(guān),因此不要認(rèn)為這里在類的內(nèi)部編寫成員函數(shù)是出于什么目的的。不要誤會了。

            讓客戶通過調(diào)用公有的非虛成員函數(shù)來間接的調(diào)用私有虛函數(shù)——這是一個基本的設(shè)計方案。我們一般將其稱為“非虛擬接口慣用法(non-virtual interface,簡寫為NVI)” 。這一方案是一個更為一般化的設(shè)計模式“模板方法”(可惜這一模式和C++中的模板并沒有什么關(guān)系)的一個特定的表現(xiàn)形式。我們將這些非虛函數(shù)(比如上文中的healthValue)稱為虛函數(shù)的“包裝器”。

            請留意上文代碼中的“準(zhǔn)備工作”、“后期工作”這兩段注釋內(nèi)容。在虛函數(shù)做實際工作的前后,這兩段注釋處的代碼肯定會得到調(diào)用。這是NVI慣用法的一個優(yōu)勢,這樣做意味著在程序調(diào)用虛函數(shù)之前,相關(guān)的背景工作已經(jīng)設(shè)定好了;在調(diào)用虛函數(shù)以后,后臺清理工作也能得以進(jìn)行,這兩項工作都是由包裝器確保進(jìn)行的。舉例說,“準(zhǔn)備工作”可能包含互斥鎖加鎖、日志記錄、確認(rèn)當(dāng)前類的約束條件和函數(shù)的先決條件是否滿足,等等。“后期工作”可能包括互斥鎖解鎖、檢查函數(shù)的后置條件、重新檢查類的約束條件,等等。如果你讓客戶直接調(diào)用虛函數(shù)的話,那么這些工作也就很難開展了。

            你也許注意到了:在使用NVI慣用法時,我們可以在派生類中對私有虛函數(shù)進(jìn)行重定義,而這些函數(shù)在派生類中是無法調(diào)用的啊!這一點看上去匪夷所思,但實際上這里并不存在設(shè)計上的矛盾。虛函數(shù)的重定義工作確認(rèn)了它實現(xiàn)其功能的方式,虛函數(shù)的調(diào)用工作確認(rèn)了它實現(xiàn)其功能的時機。兩者是相互獨立的:NVI慣用法允許在派生類中重定義虛函數(shù),這樣做使得基類將虛函數(shù)調(diào)用方式的選擇權(quán)賦予派生類,然而基類仍保留函數(shù)調(diào)用時機的決定權(quán)。這一點乍看上去有些異樣,但是,“允許派生類對私有虛函數(shù)進(jìn)行重定義”這一C++規(guī)則是非常合理的。

            在NVI慣用法的背景下,并沒有嚴(yán)格要求虛函數(shù)必須是私有的。在一些類層次結(jié)構(gòu)中,一個虛函數(shù)可能在某個派生類中的實現(xiàn)版本中調(diào)用基類的其他成員(參見條目27中SpecialWindow的例子),為了讓這樣的調(diào)用合法,這個虛函數(shù)必須聲明為受保護的,而不是私有的。還有一些情況下虛函數(shù)甚至要聲明為公有的(參見條目7:多態(tài)基類中的析構(gòu)函數(shù)),在這些情況下NVI慣用法不再可用。

            策略模式:通過函數(shù)指針實現(xiàn)

            NVI慣用法是公有虛函數(shù)的一個有趣的替代方案,但是從設(shè)計的觀點來看,這樣做無異于粉飾門面罷了。畢竟我們?nèi)匀辉谑褂锰摵瘮?shù)來計算每一個角色的生命值。這里存在著一個改進(jìn)效果更為顯著的設(shè)計主張:計算角色生命值的工作應(yīng)與角色類型完全無關(guān),于是計算生命值的工作便無須為角色類的成員。舉例說,我們可以讓每個角色類的構(gòu)造函數(shù)包含一個函數(shù)指針參數(shù),并由這一參數(shù)將生命值計算方法函數(shù)傳入,我們可以通過調(diào)用這個函數(shù)進(jìn)行實際的計算工作:

            class GameCharacter;               // 前置聲明

             

            // defaultHealthCalc函數(shù):計算生命值的默認(rèn)算法

            int defaultHealthCalc(const GameCharacter& gc);

             

            class GameCharacter {

            public:

              typedef int (*HealthCalcFunc)(const GameCharacter&);

             

              explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)

              : healthFunc(hcf)

              {}

             

              int healthValue() const

              { return healthFunc(*this); }

              ...

            private:

              HealthCalcFunc healthFunc;

            }; 

            這一方案是對另一個常見的設(shè)計模式——“策略模式”的一個簡單應(yīng)用。與使用虛函數(shù)的GameCharacter層次結(jié)構(gòu)解決方案相比而言,本方案為我們帶來了一些有趣的靈活性:

            同一角色類型的不同實例可以使用不同的生命值計算函數(shù)。比如:

            class EvilBadGuy: public GameCharacter {

            public:

              explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)

              : GameCharacter(hcf)

              { ... }

              ...

            };

            int loseHealthQuickly(const GameCharacter&);  // 不同計算方式的

            int loseHealthSlowly(const GameCharacter&);   // 生命值計算函數(shù)

             

            EvilBadGuy ebg1(loseHealthQuickly);           // 同一類型的角色

            EvilBadGuy ebg2(loseHealthSlowly);            // 使用不同類型的

                                                          // 生命值計算方法

            對特定的角色的生命值計算函數(shù)可以在運行時更改。比如說,GameCharacter可以提供一個名為setHealthCalculator的成員函數(shù),我們可以使用這個函數(shù)來更換當(dāng)前的生命值計算函數(shù)。

            另一方面,我們發(fā)現(xiàn),生命值計算函數(shù)不再是GameCharacter層次結(jié)構(gòu)的成員函數(shù)了,這一事實說明,這類生命值計算函數(shù)不再有訪問它們所處理對象的內(nèi)部成員的特權(quán)。比如說,defaultHealthCalc函數(shù)對于EvilBadGuy類中的非公有成員就沒有訪問權(quán)限。如果角色的生命值可以單純依靠角色類的公有接口所得到的信息來計算的話,上述的情況并不是問題,但是一旦精確的生命值計算需要調(diào)用非公有的信息,那么這一情況便成為問題了。事實上,凡是你使用類外部的等價功能(比如通過非成員非友元函數(shù)或通過其它類的非友元的成員函數(shù))來替代類內(nèi)部的功能(比如通過成員函數(shù))時,都存在潛在的問題。本篇余下部分內(nèi)容都存在這一問題,因為我們要考慮的其它設(shè)計方案都會涉及到GameCharacter層次結(jié)構(gòu)外部函數(shù)的使用。

            讓非成員函數(shù)訪問類內(nèi)部的非公有成員只有一個解決辦法,那就是降低類的封裝度,這是一個一般的規(guī)則。比如說,類可以將非成員函數(shù)生命為友元,類也可以為需要隱藏的具體實現(xiàn)提供公有訪問函數(shù)。使用函數(shù)指針代替虛函數(shù)可以帶來一些優(yōu)勢(比如可以為每一個角色的實例提供一個生命值計算函數(shù),可以在運行時更換計算函數(shù)),而這樣做也會降低GameCharacter的封裝度,至于優(yōu)缺點之間孰輕孰重,你在實戰(zhàn)中必須做到具體問題具體分析,盡可能的審時度勢。

            策略模式:通過tr1::function實現(xiàn)

            如果你對模板和模板的隱式接口的用法(參見條目41)很熟悉的話,那么上述的“基于函數(shù)指針”的方案就顯得十分蹩腳了。我們考慮:舊的方案必須使用一個函數(shù)來實現(xiàn)生命值計算的功能,能不能用其它一些東西(比如函數(shù)對象)來代替這個函數(shù)呢?為什么這個函數(shù)不能是成員函數(shù)呢?還有,為什么這個函數(shù)一定要返回一個整數(shù)值,而不能返回一個可以轉(zhuǎn)換成int類型的其他對象呢?

            如果我們使用一個tr1::function類型的對象來代替函數(shù)指針,那么上述問題中的約束則會瞬間瓦解。就像條目54中所解釋的,tr1::function對象可以保存任意可調(diào)用實體(也就是:函數(shù)指針、函數(shù)對象、或成員函數(shù)指針),這些實體的簽名一定與所期待內(nèi)容的類型兼容。以下是我們剛剛看到的設(shè)計方案,這次加入了tr1::function的使用:

            class GameCharacter;               // 同上

            int defaultHealthCalc(const GameCharacter& gc);

                                               // 同上

            class GameCharacter {

            public:

            // HealthCalcFunc可以是任意可調(diào)用的實體,

            // 它可以被任意與GameCharacter類型兼容的對象調(diào)用

            // 且返回值必與int類型兼容,詳情見下文

               typedef std::tr1::function<int (const GameCharacter&)>

                         HealthCalcFunc;

               explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)

               : healthFunc(hcf)

               {}

             

               int healthValue() const

               { return healthFunc(*this); }

             

               ...

             

            private:

              HealthCalcFunc healthFunc;

            };

            正如你所見到的,HealthCalcFunc是對tr1::function某個實例的一個typedef。這意味著它與一般的函數(shù)指針類型并無二致。請仔細(xì)觀察,HealthCalcFunc所表示的自定義類型:

            std::tr1::function<int(const GameCharacter&)>

            在這里,我對此tr1::function實例“目標(biāo)簽名”做加粗處理。這里目標(biāo)簽名為“一個包含const GameCharacter&參數(shù)并返回int類型的函數(shù)”。此tr1::function類型(也就是HealthCalcFunc類型)的對象可以保存兼容此目標(biāo)簽名的任意可調(diào)用實體。“兼容”意味著實體中必須包含(或可隱式轉(zhuǎn)換為)const GameCharacter&類型的參數(shù),并且此實體必須返回一個(或可隱式轉(zhuǎn)換為)int類型的返回值。

            與上文中我們剛剛看到過的設(shè)計方案(GameCharacter保存一個函數(shù)指針)相比,本方案并沒有顯著的改動。二者僅有的差別就是后一版本中GameCharacter可以保存tr1::function對象——指向函數(shù)的一般化指針。這一改變實際上是十分微不足道的,甚至有些跑題,但是,它顯著提高了客戶在程序中修改角色的生命值計算函數(shù)的靈活性,這一點還是沒有偏離議題的:

            short calcHealth(const GameCharacter&);      // 生命值計算函數(shù)

                                                          // 注意:返回值類型非int

             

            struct HealthCalculator {                    // 生命值計算類

              int operator()(const GameCharacter&) const // 函數(shù)對象

              { ... }

            };

             

            class GameLevel {

            public:

              float health(const GameCharacter&) const;  // 生命值計算成員函數(shù)

              ...                                         // 注意:返回類型非int

            };

             

            class EvilBadGuy: public GameCharacter {     // 同上

              ...

            };

            class EyeCandyCharacter: public GameCharacter {

              ...                                         // 另一個角色類型;

            };                                            // 假設(shè)與EvilBadGuy

                                                          // 有同樣的構(gòu)造函數(shù)

             

            EvilBadGuy ebg1(calcHealth);                 // 使用函數(shù)的角色

             

            EyeCandyCharacter ecc1(HealthCalculator());  // 使用函數(shù)對象的角色

             

            GameLevel currentLevel;

            ...

            EvilBadGuy ebg2( std::tr1::bind(&GameLevel::health, currentLevel, _1) );

                                                          // 使用成員函數(shù)的角色

                                                          // 詳情見下文

            我個人認(rèn)為,tr1::function可以把你帶入一個全新的美妙境界,它讓我“熱血沸騰”。如果你的熱血暫時沒有那么“沸騰”,那可能是因為你還糾結(jié)于ebg2的定義,你還不了解tr1::bind的工作機理。請允許我對它做出介紹。

            我要說的是,要計算ebg2的生命值,需要使用GameLevel類中的health成員函數(shù)。現(xiàn)在,我們所聲明的GameLevel::health函數(shù)表面上看包含一個參數(shù)(一個指向GameCharacter對象的引用),但是它實際上包含兩個參數(shù),這是因為它還包含一個隱式的GameLevel參數(shù)(this指針?biāo)笇ο螅H欢?span style="font-family:"Courier New";">GameCharacter類的生命值計算函數(shù)僅包含一個參數(shù)(需要計算生命值的GameCharacter對象)。如果我們使用GameLevel::health函數(shù)來計算ebg2的生命值的話,為了“適應(yīng)”它,我們這里只能使用一個參數(shù)(GameCharacter),而不能使用兩個(GameCharacter, GameLevel),這種情況有些蹩腳。本例中,對于ebg2的生命值計算,我們希望始終選用currentLevel作為GameLevel對象,我們可以將currentLevel作為GemeLevel對象將兩者“綁定”(bind)起來,這樣,我們在每次使用GameLevel::health函數(shù)來計算ebg2的生命值時,都可以使用綁定好的二者。這就是tr1::bind調(diào)用所做的:他告訴ebg2的生命值計算函數(shù),應(yīng)該一直指定currentLevel作為GameLevel對象。

            上文中我略去了大量細(xì)節(jié)內(nèi)容,比如說,為什么“_1”意味著“在為ebg2調(diào)用GameLevel::health時選用currentLevel作為GameLevel對象”。這些細(xì)節(jié)并不太難理解,而且它們與本節(jié)所講的基本點并無關(guān)系,通過使用tr1::function來代替函數(shù)指針,在計算角色的生命值時,我們可以讓客戶使用任意可調(diào)用實體。這是再酷不過的事情了!

            “經(jīng)典”策略模式

            如果相對于用C++來耍酷,你更加深諳設(shè)計模式之道,那么更加傳統(tǒng)的策略模式是這樣實現(xiàn)的:將生命值計算函數(shù)聲明為虛函數(shù)。并在此基礎(chǔ)上創(chuàng)建一個獨立的生命值計算層次結(jié)構(gòu)。以下的UML圖體現(xiàn)了這一設(shè)計方案:


            如果UML符號還是讓你一頭霧水的話,我就簡單點兒說吧:GameCharacter是一個繼承層次結(jié)構(gòu)中的基類,EvilBadGuyEyeCandyCharacter是派生類;HealthCalcFunc是另一個繼承層次結(jié)構(gòu)的基類,其下包含SlowHealthLoserFastHealthLoser等派生類。GameCharacter類型的每一個對象都有一個指針,這一指針指向?qū)?yīng)的HealthCalcFunc層次結(jié)構(gòu)中的對象。

            以下是對應(yīng)的代碼框架:

            class GameCharacter;               // 前置聲明

             

            class HealthCalcFunc {

            public:

              ...

              virtual int calc(const GameCharacter& gc) const

              { ... }

              ...

            };

             

            HealthCalcFunc defaultHealthCalc;

             

            class GameCharacter {

            public:

              explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)

              : pHealthCalc(phcf)

              {}

             

              int healthValue() const

              { return pHealthCalc->calc(*this);}

              ...

            private:

              HealthCalcFunc *pHealthCalc;

            };

            以上的方案可以使熟悉“標(biāo)準(zhǔn)”策略模式實現(xiàn)方式的朋友很快的識別出來,同時,此方案還可以通過為HealthCalcFunc層次結(jié)構(gòu)中添加一個派生類的方式,使對現(xiàn)有的生命值算法作出改進(jìn)成為可能。

            小節(jié)

            本條目中最基本的建議是:在你為當(dāng)前的問題設(shè)計解決方案時,不妨考慮一下虛函數(shù)以外的其他替代方案。以下是對上述替代方案的概括:

            使用“非虛擬接口”慣用法(NVI慣用法),它是模板方法設(shè)計模式的一種形式,它將公有非虛成員函數(shù)包裝成為更低訪問權(quán)限的虛函數(shù)。

            使用函數(shù)指針數(shù)據(jù)成員代替虛函數(shù)。策略設(shè)計模式一個簡裝的表現(xiàn)形式。

            使用tr1::function數(shù)據(jù)成員代替虛函數(shù)。可以使用任意可調(diào)用實體,只要實體特征與所期待的相兼容即可。同樣是策略設(shè)計模式的一種形式。

            使用另一個層次結(jié)構(gòu)中的虛函數(shù)代替同一層次結(jié)構(gòu)內(nèi)的虛函數(shù)。這是策略設(shè)計模式的傳統(tǒng)實現(xiàn)形式。

            以上并不是虛函數(shù)替代方案的完整列表,但是這足以說服你虛函數(shù)存在替代方案的。此外,這些方案的比較優(yōu)勢和劣勢,清楚的告訴你在應(yīng)用的過程中應(yīng)該對它們加以考慮。

            為了避免陷入面向?qū)ο笤O(shè)計的思維定勢,我們不妨不失時機的另辟蹊徑,所謂“條條大路通羅馬”。花些時間來研究這些替代方案還是頗有裨益的。

            時刻牢記

            虛函數(shù)的替代方案包括:NVI慣用法和策略模式的不同實現(xiàn)方式。NVI慣用法是模板方法設(shè)計模式的一個實例。

            將成員函數(shù)的功能挪至類外部存在著以下缺點:非成員函數(shù)對類的非公有成員沒有訪問權(quán)限。

            tr1::function對象就像更一般化的函數(shù)指針。這類對象支持給定特征的任意可調(diào)用實體。

            posted on 2011-12-25 00:59 ★ROY★ 閱讀(3264) 評論(2)  編輯 收藏 引用 所屬分類: Effective C++

            評論

            # re: 【讀書筆記】[Effective C++中文版第3版][第35條]為虛函數(shù)尋求替代方案   回復(fù)  更多評論   

            不錯,剛一看還以為是樓主寫的,那不得了的。
            好多年沒看過effective c++了,想不到這第3版確實改進(jìn)不少。
            2011-12-25 12:01 | 春秋十二月

            # re: 【讀書筆記】[Effective C++中文版第3版][第35條]為虛函數(shù)尋求替代方案   回復(fù)  更多評論   

            用非虛函數(shù)來包裝虛函數(shù)這個做法雖然早就聽過, 但是我一直沒明白什么場合需要用. 到使用虛函數(shù)包裝非虛函數(shù)的"代理"技術(shù), 倒是時不常的能用到.
            2011-12-25 19:08 | 欲三更
            久久久久亚洲av无码专区| 久久99这里只有精品国产| 久久电影网一区| 国内精品伊人久久久久网站| 人妻少妇精品久久| 色妞色综合久久夜夜| 久久久久久综合一区中文字幕| 欧美日韩成人精品久久久免费看| 亚洲AV日韩AV永久无码久久| 一本久久a久久精品综合夜夜| 国产精品成人久久久| 97精品伊人久久久大香线蕉| 久久精品成人欧美大片| 国产精品99久久精品爆乳| 无码伊人66久久大杳蕉网站谷歌 | 国产高潮久久免费观看| 久久久久亚洲?V成人无码| 国产精品久久成人影院| 偷窥少妇久久久久久久久| 亚洲国产精品久久| 久久综合狠狠综合久久| 伊人久久大香线蕉AV一区二区| 91久久香蕉国产熟女线看| 久久精品国产免费观看| 99久久综合国产精品免费| 亚洲国产日韩欧美久久| 久久国产精品二国产精品| 成人a毛片久久免费播放| 久久精品一区二区国产| 久久99精品久久只有精品| 久久婷婷五月综合色奶水99啪| 久久这里的只有是精品23| 亚洲精品国精品久久99热| 色婷婷久久综合中文久久一本| 伊人色综合久久天天| 99国内精品久久久久久久| 青青青国产成人久久111网站| 久久久国产精品福利免费 | 精品国产乱码久久久久久浪潮| 国产精品激情综合久久| 久久久亚洲精品蜜桃臀|