• <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>
            隨筆-341  評論-2670  文章-0  trackbacks-0

            在思考怎么寫這一篇文章的時候,我又想到了以前討論正交概念的事情。如果一個系統(tǒng)被設(shè)計成正交的,他的功能擴(kuò)展起來也可以很容易的保持質(zhì)量這是沒錯的,但是對于每一個單獨(dú)給他擴(kuò)展功能的個體來說,這個系統(tǒng)一點都不好用。所以我覺得現(xiàn)在的語言被設(shè)計成這樣也是有那么點道理的。就算是設(shè)計Java的那誰,他也不是傻逼,那為什么Java會被設(shè)計成這樣?我覺得這跟他剛開始想讓金字塔的底層程序員也可以順利使用Java是有關(guān)系的。

            ?

            難道好用的語言就活該不好擴(kuò)展碼?實際上不是這樣的,但是這仍然是上面那個正交概念的問題。一個容易擴(kuò)展的語言要讓你覺得好用,首先你要投入時間來學(xué)習(xí)他。如果你想簡單的借鑒那些不好擴(kuò)展的語言的經(jīng)驗(如Java)來在短時間內(nèi)學(xué)會如何使用一個容易擴(kuò)展的語言(如C++/C#)——你的出發(fā)點就已經(jīng)投機(jī)了。所以這里有一個前提值得我再強(qiáng)調(diào)一次——首先你需要投入時間去學(xué)習(xí)他。

            ?

            正如我一直在群里說的:"C++需要不斷的練習(xí)——vczh"。要如何練習(xí)才能讓自己借助語言做出一個可擴(kuò)展的架構(gòu)呢?先決條件就是,當(dāng)你在練習(xí)的時候,你必須是在練習(xí)如何實現(xiàn)一個從功能上就要求你必須保證他的可擴(kuò)展性的系統(tǒng),舉個例子,GUI庫就是其中的一類。我至今認(rèn)為,學(xué)會實現(xiàn)一個GUI庫,比通過練習(xí)別的什么東西來提高自己的能力來講,簡直就算一個捷徑了。

            ?

            那么什么是擴(kuò)展呢?簡單的來講,擴(kuò)展就是在不修改原有代碼的情況下,僅僅通過添加新的代碼,就可以讓原有的功能適應(yīng)更多的情況。一般來講,擴(kuò)展的主要目的并不是要增加新的功能,而是要只增加新代碼的前提下修改原有的功能。譬如說原來你的系統(tǒng)只支持SQLServer,結(jié)果有一天你遇到了一個喜歡Oracle的新客戶,你要把東西賣給他,那就得支持Oracle了吧。但是我們知道,SQLServer和Oracle在各種協(xié)議(asp.net、odbc什么的)上面是有偏好的,用DB不喜歡的協(xié)議來連接他的時候bug特別多,這就造成了你又可能沒辦法使用單一的協(xié)議來正確的使用各種數(shù)據(jù)庫,因此擴(kuò)展的這個擔(dān)子就落在你的身上了。當(dāng)然這種系統(tǒng)并不是人人都要寫,我也可以換一個例子,假如你在設(shè)計一個GPU集群上的程序,那么這個集群的基礎(chǔ)架構(gòu)得支持NVidia和AMD的顯卡,還得支持DirectCompute、Cuda和OpenCL。然而我們知道,OpenCL在不同的平臺上,有互不兼容的不同的bug,導(dǎo)致你實際上并不可能僅僅通過一份不變的代碼,就充分發(fā)揮OpenCL在每一個平臺上的最佳狀態(tài)……現(xiàn)實世界的需求真是orz(OpenCL在windows上用AMD卡定義一個struct都很容易導(dǎo)致崩潰什么的,我覺得這根本不能用)……

            ?

            在語言里面談擴(kuò)展,始終都離不開兩個方面:編譯期和運(yùn)行期。這些東西都是用看起來很像pattern matching的方法組織起來的。如果在語言的類型系統(tǒng)的幫助下,我們可以輕松做出這樣子的架構(gòu),那這個語言就算有可擴(kuò)展的類型了。

            ?

            1. 編譯期對類型的擴(kuò)展

            ?

            這個其實已經(jīng)被人在C++和各種靜態(tài)類型的函數(shù)式語言里面做爛了。簡單的來講,C++處理這種問題的方法就是提供偏特化。可惜C++的偏特化只讓做在class上面,結(jié)果因為大家對class的誤解很深,順便連偏特化這種比OO簡單一萬倍的東西也誤解了。偏特化不允許用在函數(shù)上,因為函數(shù)已經(jīng)有了重載,但是C++的各種標(biāo)準(zhǔn)在使用函數(shù)來擴(kuò)展類型的時候,實際上還是當(dāng)他是偏特化那么用的。我舉個例子。

            ?

            C++11多了一個foreach循環(huán),寫成for(auto x : xs) { … }。STL的類型都支持這種新的for循環(huán)。C++11的for循環(huán)是為了STL的容器設(shè)計的嗎?顯然不是。你也可以給你自己寫的容器加上for循環(huán)。方法有兩種,分別是:1、給你的類型T加上T::begin和T::end兩個成員函數(shù);2、給你的類型T實現(xiàn)begin(T)和end(T)兩個全局函數(shù)。我還沒有去詳細(xì)考證,但是我認(rèn)為缺省的begin(T)和end(T)全局函數(shù)就是去調(diào)用T::begin和T::end的,因此for循環(huán)只需要認(rèn)begin和end兩個全局函數(shù)就可以了。

            ?

            那自己的類型怎么辦呢?當(dāng)然也要去重載begin和end了。現(xiàn)在全局函數(shù)沒有重載,因此寫出來大概是:
            template<typename T> auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }

            template<typename T> my_iterator<T> begin(const my_container<T>& t);

            template<typename T> my_range_iterator<T> begin(pair<T, T> range);

            ?

            如果C++的函數(shù)支持偏特化的話,那么上面這段代碼就會被改成這樣,而且for循環(huán)也就不去找各種各樣的begin函數(shù)了,而只認(rèn)定那一個std::begin就可以了:
            template<typename T> auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }

            template<typename T> my_iterator<T> begin< my_container<T>>(const my_container<T>& t);

            template<typename T> my_range_iterator<T> begin< pair<T, T>>( const pair<T, T>& range);

            ?

            為什么要偏特化呢?因為這至少保證你寫出來的begin函數(shù)跟for函數(shù)想要的begin函數(shù)的begin函數(shù)的簽名是相容的(譬如說不能有兩個參數(shù)之類的)。事實上C++11的for循環(huán)剛開始是要求大家通過偏特化一個叫做std::range的類型來支持的,這個range類型里面有兩個static函數(shù),分別叫begin和end。后來之所以改成這樣,我猜大概是因為C++的每一個函數(shù)重載也可以是模板函數(shù),因此就不需要引入一個新的類型了,就讓大家去重載好了。而且for做出來的時候,C++標(biāo)準(zhǔn)里面還沒有concept,因此也沒辦法表達(dá)"對于所有可以循環(huán)的類型T,我們都有std::range<T>必須滿足這個叫做range_loopable<T>的concept"這樣的前置條件。

            ?

            重載用起來很容易讓人走火入門,很多人到最后都會把一些僅僅看起來像而實際上語義完全不同的東西用重載來表達(dá),函數(shù)的參數(shù)連相似性都沒有。其實這是不對的,這種時候就應(yīng)該把函數(shù)改成兩個不同的名字。假如當(dāng)初設(shè)計C++的是我,那我一定會把函數(shù)重載干掉,然后允許人們對函數(shù)進(jìn)行偏特化,并且加上concept。既然std::begin已經(jīng)被定義為循環(huán)的輔助函數(shù)了,那么你重載一個std::begin,他卻不能用來循環(huán)(譬如說有兩個參數(shù)什么的),那有意義嗎?完全沒有。

            ?

            這種例子還有很多,譬如如何讓自己的類型可以被<<到wcout的做法啦,boost的那個serialization框架,還有各種各樣的庫,其實都利用了相同的思想——對類型做編譯期的擴(kuò)展,使用一些手段使得在不需要修改原來的代碼的前提下,就可以讓編譯器找到你新加進(jìn)去的函數(shù),從而使得調(diào)用的寫法不用發(fā)生變化就可以對原有的功能支持更多的情況。至少我們讓我們自己的類型支持for循環(huán)就不需要翻開std::begin的代碼把我們的類型寫進(jìn)去,只需要在隨便什么空白的地方重載一個std::begin就可以了。這就是一個很好地體現(xiàn)。C++的標(biāo)準(zhǔn)庫一直在引導(dǎo)大家正確設(shè)計一個可擴(kuò)展的架構(gòu),可惜很多人都意識不到這一點,為了自己那一點連正確性都談不上的強(qiáng)迫癥,放棄了很多東西。

            ?

            很多靜態(tài)類型的函數(shù)式語言使用concept來完成上述的工作。當(dāng)一個concept定義好了之后,我們就可以通過對concept的實現(xiàn)進(jìn)行偏特化來讓我們的類型T滿足concept的要求,來讓那些調(diào)用這個concept的泛型代碼,可以在處理的對象是T的時候,轉(zhuǎn)而調(diào)用我們提供的實現(xiàn)。Haskell就是一個典型的例子,一個sort函數(shù)必然要求元素是可比較的,一個可以比較的類型定義為實現(xiàn)了Ord這個type class的類型。所以你只要給你自己的類型T實現(xiàn)Ord這個type class,那sort函數(shù)就可以對T的列表進(jìn)行排序了。

            ?

            對于C++和C#這種沒有concept或者concept不是主要概念的語言里面,對類型做靜態(tài)的擴(kuò)展只需要你的類型滿足"我可以這么這么干"就可以了。譬如說你重載一個begin和end,那你的類型就可以被foreach;你給你的類型實現(xiàn)了operator<等函數(shù),那么一個包含你的類型的容器就可以被sort;或者C#的只要你的類型T<U>有一大堆長得跟System.Linq.Enumerable里面定義的擴(kuò)展函數(shù)一樣的擴(kuò)展函數(shù),那么Linq的神奇的語法就可以用在你的類型上等等。這跟動態(tài)類型的"只要它長的像鴨子,那么它就是鴨子"的做法有異曲同工之效。如果你的begin函數(shù)的簽名沒寫對,編譯器也不會屌你,直到你對他for的時候編譯器才會告訴你說你做錯了。這跟很多動態(tài)類型的語言的很多錯誤必須在運(yùn)行的時候才發(fā)現(xiàn)的性質(zhì)也是類似的。

            ?

            Concept對于可靜態(tài)擴(kuò)展的類型的約束,就如同類型對于邏輯的約束一樣。沒有concept的C++模板,就跟用動態(tài)類型語言寫邏輯一樣,只有到用到的那一刻你才知道你到底寫對了沒有,而且錯誤也會爆發(fā)在你使用它的地方,而不是你定義它的地方。因此本著編譯器幫你找到盡可能多的錯誤的原則,C++也開始有concept了。

            ?

            C#的擴(kuò)展方法用在Linq上面,其實編譯器也要求你滿足一個內(nèi)在的concept,只是這個概念無法用C#的語法表達(dá)出來。所以我們在寫Linq Provider的時候也會有同樣的感覺。Java的interface都可以寫缺省實現(xiàn)了,但是卻沒有靜態(tài)方法。這就造成了我們實際上無法跟C++和C#一樣,在不修改原有代碼的前提下,讓原有的功能滿足更多的情況。因為C#的添加擴(kuò)展方法的情況,到了Java里面就變成讓一個類多繼承自一個interface,必須修改代碼了。Java的這個功能特別的雞肋,不知道是不是他故意想跟C#不一樣才設(shè)計成這個樣子的,可惜精華沒有抄去,卻抄了糟粕。

            ?

            1. 運(yùn)行期對類型的擴(kuò)展

            ?

            自從Java吧靜態(tài)類型和面向?qū)ο罄壴谝黄鹬螅瑯I(yè)界對"運(yùn)行期對類型的擴(kuò)展"這個主題思考了很多年,甚至還出了一本著作叫《設(shè)計模式》,讓很多人捧為經(jīng)典。大家爭先恐后的學(xué)習(xí),而效果卻不怎么樣。這是因為《設(shè)計模式》不好嗎?不是。這是因為靜態(tài)類型和面向?qū)ο罄壴谝黄鹬螅O(shè)計一個可擴(kuò)展的架構(gòu)就很難嗎?也不是。真正的原因是,Java設(shè)計(好像也是抄的Simular?我記不太清楚了)的虛函數(shù)把這個問題的難題提升了一個等級。

            ?

            用正確的概念來理解問題可以讓我們更容易的掌握問題的本質(zhì)。語言是有魔力的,習(xí)慣說中文的人,思考方式都跟中國人差不多。習(xí)慣說英語的人,思考方式都跟美國人差不多。因此習(xí)慣了使用C++/C#/Java的人,他們對于面向?qū)ο蟮南敕ㄆ鋵嵰彩遣畈欢嗟摹_@是人類的天性。盡管大家鼓吹說語言只是工具,我們應(yīng)該掌握方法論什么的,但是這就跟要求男人面對一個萌妹紙不勃起一樣,違背了人類的本性,難度簡直太高了。于是我今天從虛函數(shù)和Visitor模式講起,告訴大家為什么虛函數(shù)的這種形式會讓"擴(kuò)展的時候不修改原有的代碼"變難。

            ?

            絕大多數(shù)的系統(tǒng)的擴(kuò)展,都可以最后化簡(這并不要求你非得這么做)為"當(dāng)它的類型是這個的時候你就干那個"的這么件事。對于在編譯的時候就已經(jīng)知道的,我們可以用偏特化的方法讓編譯器在生成代碼的時候就先搞好。對于運(yùn)行的時候,你拿到一個基類(其實為什么一定要有基類?應(yīng)該有的是interface!參見上一篇文章——刪減語言的功能),那如何O(1)時間復(fù)雜度(這里的n指的是所有跟這次跳轉(zhuǎn)有關(guān)系的類型的數(shù)量)就跳轉(zhuǎn)到你想要的那個分支上去呢?于是我們有了虛函數(shù)。

            ?

            靜態(tài)的擴(kuò)展用的是靜態(tài)的分派,于是編譯器幫我們把函數(shù)名都hardcode到生成的代碼里面。動態(tài)的類型用的是動態(tài)的分派,于是我們得到的當(dāng)然是一個相當(dāng)于函數(shù)指針的東西。于是我們會把這個函數(shù)指針保存在從基類對象可以O(shè)(1)訪問到的地方。虛函數(shù)就是這么實現(xiàn)的,而且這種類型的分派必須要這么實現(xiàn)的。但是,寫成代碼就一定要寫程序函數(shù)嗎

            ?

            其實本來沒什么理由讓一個語言(或者library)長的樣子必須有提示你他是怎么實現(xiàn)的功能。關(guān)心太多容易得病,執(zhí)著太多心生痛苦啊。所以好好的解決問題就好了。至于原理是什么,下了班再去關(guān)心。估計還有一些人不明白為什么不好,我就舉一個通俗的例子。我們都知道dynamic_cast的性能不怎么樣,虛函數(shù)用來做if的性能要遠(yuǎn)遠(yuǎn)比dynamic_cast用來做if的性能好得多。因此下面所有的答案都基于這個前提——要快,不要dynamic_cast!

            ?

            1. 處理HTML

            ?

            好了,現(xiàn)在我們的任務(wù)是,拿到一個HTML,然后要對他做一些功能,譬如說把它格式化成文本啦,看一下他是否包含超鏈接啦等等。假設(shè)我們已經(jīng)解決HTML的語法分析問題,那么我們會得到一顆靜態(tài)類型的語法樹。這棵語法樹如無意外一定是長下面這個樣子的。另外一種選擇是存成動態(tài)類型的,但是這跟面向?qū)ο鬅o關(guān),所以就不提了。

            ?

            class DomBase

            {

            public:

            ????virtual ~DomBase();

            ?

            ????static shared_ptr<DomBase> Parse(const wstring& htmlText);

            };

            ?

            class DomText : public DomBase{};

            class DomImg : public DomBase{};

            class DomA : public DomBase{};

            class DomDiv : public DomBase{};

            ......

            ?

            HTML的tag種類繁多,大概有那么上百個吧。那現(xiàn)在我們要給他加上一個格式化成字符串的功能,這顯然是一個遞歸的算法,先把sub tree一個一個格式化,最后組合起來就好了。可能對于不同的非文本標(biāo)簽會有不同的格式化方法。代碼寫出來就是這樣——基本上是唯一的作法:

            ?

            class DomBase

            {

            public:

            ????virtual ~DomBase();

            ????static shared_ptr<DomBase> Parse(const wstring& htmlText);

            ?

            ????virtual void FormatToText(ostream& o); // 默認(rèn)實現(xiàn),把所有subtree的結(jié)果合并

            };

            ?

            class DomText : public DomBase

            {

            public:

            ????void FormatToText(ostream& o); // 直接輸出文字

            };

            class DomImg : public DomBase

            {

            public:

            ????void FormatToText(ostream& o); // 輸出imgtag內(nèi)容

            };

            // 其它實現(xiàn)略

            class DomA : public DomBase{};

            class DomDiv : public DomBase{};

            ?

            這已經(jīng)構(gòu)成一個基本的HTML的Dom Tree了。現(xiàn)在我提一個要求如下,要求在不修改原有代碼只添加新代碼的情況下,避免dynamic_cast,實現(xiàn)一個考察一顆Dom Tree是否包含超鏈接的功能。能做嗎?

            ?

            無論大家如何苦思冥想,答案都是做不到。盡管這么一看可能覺得這不是什么大事,但實際上這意味著:你無法通過添加模塊的方式來給一個已知的Dom Tree添加"判斷它是否包含超鏈接"的這個功能。有的人可能會說,那把它建模成動態(tài)類型的樹不就可以了?這是沒錯,但這實際上有兩個問題。第一個是著顯著的增加了你的測試成本,不過對于充滿了廉價勞動力的web行業(yè)來說這好像也不是什么大問題。第二個更加本質(zhì)——HTML可以這么做,并不代表所有的東西都可以裝怎么做事吧。

            ?

            那在靜態(tài)類型的前提下,要如何解決這個問題呢?很久以前我們的《設(shè)計模式》就給我們提供了visitor模式,用來解決這樣的問題。如果把這個Dom Tree修改成visitor模式的代碼的話,那原來FormatToText就會變成這個樣子:

            ?

            class DomText;

            class DomImg;

            class DomA;

            class DomDiv;

            ?

            class DomBase

            {

            public:

            ????virtual ~DomBase();

            ????static shared_ptr<DomBase> Parse(const wstring& htmlText);

            ?

            ????class IVisitor

            ????{

            ????public:

            ????????virtual ~IVisitor();

            ?

            ????????virtual void Visit(DomText* dom) = 0;

            ????????virtual void Visit(DomImg* dom) = 0;

            ????????virtual void Visit(DomA* dom) = 0;

            ????????virtual void Visit(DomDiv* dom) = 0;

            ????};

            ?

            ????virtual void Accept(IVisitor* visitor) = 0;

            };

            ?

            class DomText : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            class DomImg : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            class DomA : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            class DomDiv : public DomBase

            {

            public:

            ????void Accept(IVisitor* visitor)override

            ????{

            ????????visitor->Visit(this);

            ????}

            };

            ?

            class FormatToTextVisitor : public DomBase::IVisitor

            {

            private:

            ????ostream& o;

            public:

            ????FormatToTextVisitor(ostream& _o)

            ????????:o(_o)

            ????{

            ?

            ????}

            ?

            ????void Visit(DomText* dom){} // 直接輸出文字

            ????void Visit(DomImg* dom){} // 輸出imgtag內(nèi)容

            ????void Visit(DomA* dom){} // 默認(rèn)實現(xiàn),把所有subtree的結(jié)果合并

            ????void Visit(DomDiv* dom){} // 默認(rèn)實現(xiàn),把所有subtree的結(jié)果合并

            ?

            ????static void Evaluate(DomBase* dom, ostream& o)

            ????{

            ????????FormatToTextVisitor visitor(o);

            ????????dom->Accept(&visitor);

            ????}

            };

            ?

            看起來長了不少,但是我們驚奇地發(fā)現(xiàn),這下子我們可以通過提供一個Visitor,來在不修改原有代碼的前提下,避免dynamic_cast,實現(xiàn)判斷一顆Dom Tree是否包含超鏈接的功能了!不過別高興得太早。這兩種做法都是有缺陷的。

            ?

            虛函數(shù)的好處是你可以在不修改原有代碼的前提下添加新的Dom類型,但是所有針對Dom Tree的操作緊密的耦合在了一起,并且邏輯還分散在了每一個具體的Dom類型里面。你添加一個新功能就要修改所有的DomBase的子類,因為你要給他們都添加你需要的虛函數(shù)。

            ?

            Visitor的好處是你可以在不修改原有代碼的前提下添加新的Dom操作,但是所有的Dom類型卻緊密的耦合在了一起,因為IVisitor類型要包含所有DomBase的子類。你每天加一個新的Dom類型就得修改所有的操作——即IVisitor的接口和所有的具體的Visitor。而且還有另一個問題,就是虛函數(shù)的默認(rèn)實現(xiàn)寫起來比較鳥了

            ?

            所以這兩種做法都各有各的耦合。

            ?

            1. 碰撞系統(tǒng)

            ?

            看了上面對于虛函數(shù)和Visitor的描述,大家大概知道了虛函數(shù)和Visitor其實都是同一個東西,只是各有各的犧牲。因此他們是可以互相轉(zhuǎn)換的——大家通過不斷地練習(xí)就可以知道如何把一個解法表達(dá)成虛函數(shù)的同時也可以表達(dá)成Visitor了。但是Visitor的代碼又臭又長,所以下面我只用虛函數(shù)來寫,懶得敲太多代碼了。

            ?

            虛函數(shù)只有一個this參數(shù),所以他是single dynamic dispatch。對于碰撞系統(tǒng)來說,不同種類的物體之間的碰撞代碼都是不一樣的,所以他有兩個"this參數(shù)",所以他是multiple dynamic dispatch。在接下來的描述會發(fā)現(xiàn),只要遇上了multiple dynamic dispatch,在現(xiàn)有的架構(gòu)下避免dynamic_cast,無論你用虛函數(shù)還是visitor模式,做出來的solution全都是不管操作有沒有偶合在一起,反正類型是肯定會偶合在一起的。

            ?

            現(xiàn)在我們面對的問題是這樣的。在物理引擎里面,我們經(jīng)常需要判斷兩個物體是否碰撞。但是物體又不只是三角形組成的多面體,還有可能是標(biāo)準(zhǔn)的球形啊、立方體什么的。因此這顯然還是一個繼承的結(jié)構(gòu),而且還有一個虛函數(shù)用來判斷一個對象跟另一個對象是否碰撞:

            ?

            class Geometry

            {

            public:

            ????virtual ~Geometry();

            ?

            ????virtual bool IsCollided(Geometry* second) = 0;

            };

            ?

            class Sphere : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????// then ???

            ????}

            };

            ?

            class Cube : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????// then ???

            ????}

            };

            ?

            class Triangles : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????// then ???

            ????}

            };

            ?

            大家猛然發(fā)現(xiàn),在這個函數(shù)體里面也不知道second到底是什么東西。這意味著,我們還要對second做一次single dynamic dispatch,這也就意味著我們需要添加新的虛函數(shù)。而且這不是一個,而是很多。他們分別是什么呢?由于我們已經(jīng)對first(也就是那個this指針)dispatch過一次了,所以我們要把dispatch的結(jié)果告訴second,要讓它在dispatch一次。所以當(dāng)first分別是Sphere、Cube和Triangles的時候,對second的dispatch應(yīng)該有不同的邏輯。因此很遺憾的,代碼會變成這樣:

            ?

            class Sphere;

            class Cube;

            class Triangles;

            ?

            class Geometry

            {

            public:

            ????virtual ~Geometry();

            ?

            ????virtual bool IsCollided(Geometry* second) = 0;

            ????virtual bool IsCollided_Sphere(Sphere* first) = 0;

            ????virtual bool IsCollided_Cube(Cube* first) = 0;

            ????virtual bool IsCollided_Triangles(Triangles* first) = 0;

            };

            ?

            class Sphere : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????return second->IsCollided_Sphere(this);

            ????}

            ?

            ????bool IsCollided_Sphere(Sphere* first)override

            ????{

            ????????// Sphere * Sphere

            ????}

            ?

            ????bool IsCollided_Cube(Cube* first)override

            ????{

            ????????// Cube * Sphere

            ????}

            ?

            ????bool IsCollided_Triangles(Triangles* first)override

            ????{

            ????????// Triangles * Sphere

            ????}

            };

            ?

            class Cube : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????return second->IsCollided_Cube(this);

            ????}

            ?

            ????bool IsCollided_Sphere(Sphere* first)override

            ????{

            ????????// Sphere * Cube

            ????}

            ?

            ????bool IsCollided_Cube(Cube* first)override

            ????{

            ????????// Cube * Cube

            ????}

            ?

            ????bool IsCollided_Triangles(Triangles* first)override

            ????{

            ????????// Triangles * Cube

            ????}

            };

            ?

            class Triangles : public Geometry

            {

            public:

            ????bool IsCollided(Geometry* second)override

            ????{

            ????????return second->IsCollided_Triangles(this);

            ????}

            ?

            ????bool IsCollided_Sphere(Sphere* first)override

            ????{

            ????????// Sphere * Triangles

            ????}

            ?

            ????bool IsCollided_Cube(Cube* first)override

            ????{

            ????????// Cube * Triangles

            ????}

            ?

            ????bool IsCollided_Triangles(Triangles* first)override

            ????{

            ????????// Triangles * Triangles

            ????}

            };

            ?

            大家可以想象,如果還有第三個Geometry參數(shù),那還得給Geometry加上9個新的虛函數(shù),三個子類分別實現(xiàn)他們,加起來我們一共要寫13個虛函數(shù)(3^0 + 3^1 + 3^2)39個函數(shù)體(3^1 + 3^2 + 3^3)。

            ?

            1. 結(jié)尾

            ?

            為什么運(yùn)行期的類型擴(kuò)展就那么多翔,而靜態(tài)類型的擴(kuò)展就不會呢?原因是靜態(tài)類型的擴(kuò)展是寫在類型的外部的。假設(shè)一下,我們的C++支持下面的寫法:

            ?

            bool IsCollided(switch Geometry* first, switch Geometry* second);

            bool IsCollided(case Sphere* first, case Sphere* second);

            bool IsCollided(case Sphere* first, case Cube* second);

            bool IsCollided(case Sphere* first, case Triangles* second);

            bool IsCollided(case Cube* first, case Sphere* second);

            bool IsCollided(case Cube* first, case Cube* second);

            bool IsCollided(case Cube* first, case Triangles* second);

            bool IsCollided(case Triangles* first, case Sphere* second);

            bool IsCollided(case Triangles* first, case Cube* second);

            bool IsCollided(case Triangles* first, case Triangles* second);

            ?

            最后編譯器在編譯的時候,把所有的"動態(tài)偏特化"收集起來——就像做模板偏特化的時候一樣——然后替我們生成上面一大片翔一樣的虛函數(shù)的代碼,那該多好啊!

            ?

            Dynamic dispatch和解耦這從一開始以來就是一對矛盾,要徹底解決他們其實是很難的。雖然上面的作法看起來類型和操作都解耦了,可實際上這就讓我們失去了本地代碼的dll的功能了。因為編譯器不可能收集到以后才動態(tài)鏈接進(jìn)來的dll代碼里面的"動態(tài)偏特化"的代碼對吧。不過這個問題對于像CLR一樣基于一個VM一樣的支持JIT的runtime來講,這其實并不是個大問題。而且Java的J2EE也好,Microsoft的Enterprise Library也好,他們的IoC(Inverse of Control)其實也是在模擬這個寫法。我認(rèn)為以后靜態(tài)類型語言的方向,肯定是朝著這個路線走的。盡管這些概念再也不能被直接map到本地代碼了,但是這讓我們從語義上的耦合中解放了出來,對于寫需要穩(wěn)定執(zhí)行的大型程序來說,有著莫大的助。

            posted on 2013-11-10 01:06 陳梓瀚(vczh) 閱讀(10100) 評論(6)  編輯 收藏 引用 所屬分類: 啟示

            評論:
            # re: 如何設(shè)計一門語言(十二)——設(shè)計可擴(kuò)展的類型 2013-11-10 06:49 | lichray
            range-for 只找 ADL begin/end,既不是全局也不是 T::begin/end.
            你那個例子用全特化寫也一樣。
            std::range 和 range-for 從沒扯上關(guān)系過。前者是 range study group 的開端,不過其本身被迅速否決了。  回復(fù)  更多評論
              
            # re: 如何設(shè)計一門語言(十二)——設(shè)計可擴(kuò)展的類型 2013-11-10 14:36 | lichray
            好吧,我知道你說的需要「函數(shù)模板偏特化」的意思了。  回復(fù)  更多評論
              
            # re: 如何設(shè)計一門語言(十二)——設(shè)計可擴(kuò)展的類型 2013-11-10 21:06 | 幻の上帝
            序號怎么都是1……
            反了。不就是要模式匹配么。還得事先糾結(jié)編譯期和運(yùn)行時的區(qū)別?
            關(guān)于函數(shù)模板偏特化和重載之間的取舍,你低估了一個成本:特化對代碼出現(xiàn)位置要求很死板,比如作用域問題,再如考慮extern template往往需要小心調(diào)整順序;重載就沒這么多條條框框,算上ADL用起來多快好省。
            C++重載的邪惡之處在于考慮一般情況本身的規(guī)則就非常羅嗦(尤其是各種transformation混搭的overloading resolution),用起來經(jīng)常讓人沒底,實現(xiàn)起來也麻煩。
            至于不應(yīng)該用重載的地方用了重載,我只能說,不作死就不會死。但是另一方面,看到別人的渣代碼因為更嚴(yán)格的類型規(guī)則的限制反而用了不夠準(zhǔn)確表達(dá)意圖的類型,可修改性通常更拙計,造成的麻煩往往更大。
            這里所謂的鴨子,一般地說不就是structural typing么。本質(zhì)的優(yōu)缺點和上面類似。

              回復(fù)  更多評論
              
            # re: 如何設(shè)計一門語言(十二)——設(shè)計可擴(kuò)展的類型 2013-11-10 21:37 | 幻の上帝
            @lichray
            range-based for不是先找成員begin/end再ADL的么。  回復(fù)  更多評論
              
            # re: 如何設(shè)計一門語言(十二)——設(shè)計可擴(kuò)展的類型 2013-11-11 03:56 | 陳梓瀚(vczh)
            @幻の上帝
            說實話這是C++把偏特化做成這樣造成的,而不是偏特化本身有什么問題。再說了加上concept就不是structural typing  回復(fù)  更多評論
              
            # re: 如何設(shè)計一門語言(十二)——設(shè)計可擴(kuò)展的類型 2015-02-19 05:23 | earthengine
            其實編譯器如果要實現(xiàn)C++的動態(tài)分發(fā)的話,策略還是有的。首先,連接器需要支持“組合數(shù)據(jù)區(qū)段”的功能,同一個符號可以包含不同的部分分散在不同的目標(biāo)文件,并且自動生成一個索引。其次,對于每一組需要多重分發(fā)的函數(shù),要建立分層次的分發(fā)表。當(dāng)IsCollided(x,y)被調(diào)用時,先找x的虛函數(shù)表,從中找到IsCollided分發(fā)表的位置,再從y的虛函數(shù)表中找出一個索引。這樣在x的IsColided分發(fā)表里面就可以找到真正函數(shù)的位置。

            如果有3個參數(shù)x,y,z要動態(tài)分發(fā),則上面過程中找到的只是2級分發(fā)表的位置,還需要結(jié)合z虛函數(shù)表里面的索引值來找到2級分發(fā)表表項,那個才是真正的函數(shù)位置。

            這就是為什么連接器需要自動生成索引,因為以上的分發(fā)表必須在連接時生成,而且可能跨越不同的目標(biāo)文件。除此之外,還必須在生成虛函數(shù)表和分發(fā)表的時候允許字段的值到連接時才確定,因為不到連接時刻索引值都是不確定的。  回復(fù)  更多評論
              
            国产亚洲美女精品久久久久狼| 久久久亚洲欧洲日产国码二区| 久久成人18免费网站| 理论片午午伦夜理片久久| 中文字幕久久波多野结衣av| 777米奇久久最新地址| 久久久久久国产精品免费免费| 国产成人精品综合久久久久| 99re久久精品国产首页2020| 久久久久久国产精品美女| 亚洲国产精品无码成人片久久| 国产69精品久久久久9999| 尹人香蕉久久99天天拍| www.久久热| 香蕉久久久久久狠狠色| 久久电影网一区| 一本一本久久A久久综合精品 | 国产精品永久久久久久久久久| 色青青草原桃花久久综合| 久久er国产精品免费观看2| 久久亚洲精品国产精品婷婷| 久久国产精品99精品国产987| 亚洲欧美日韩精品久久亚洲区| 久久精品a亚洲国产v高清不卡| 一本久道久久综合狠狠躁AV| 国产亚洲美女精品久久久久狼| 亚洲国产精品无码久久久蜜芽| 久久国产免费| 国产精品成人精品久久久 | 777米奇久久最新地址| 狠狠色丁香婷婷久久综合五月| 精品久久人人爽天天玩人人妻| 69久久夜色精品国产69| 亚洲中文字幕无码久久2017| 久久久青草青青国产亚洲免观| 亚洲综合婷婷久久| 久久精品国产影库免费看 | 久久香蕉国产线看观看猫咪?v| 国产精品一区二区久久国产| 午夜精品久久久久久久| 午夜精品久久久久久毛片|