• <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>

            longshanks

              C++博客 :: 首頁 :: 聯系 :: 聚合  :: 管理
              14 Posts :: 0 Stories :: 214 Comments :: 0 Trackbacks

            常用鏈接

            留言簿(10)

            我參與的團隊

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            2008年9月18日 #

            三只小豬
            莫華楓

                小時候聽說過三只小豬的故事,隱約記得故事是講三只小豬用不同方法造房子,對抗老狼。這些天做軟件,遇到一個無比簡單的問題,但在三種不同的語言中,卻有著截然不同的解法。

                最近,冷不丁地接到公司下派的一個緊急任務,做手持POS和PC程序之間交換數據的程序。各種各樣的麻煩中,有一個小得不起眼的問題,POS機中數據的字 節序和PC相反。這可不算是什么啊。沒錯,是在太簡單了。盡管如此,還是引發了一場爭論。做POS程序的,希望PC程序做轉換。做PC程序的,希望POS 程序做轉換。(誰都想少做點,對吧;))。最終,作為和事佬的我,為了維護和諧的氛圍,攬下了這件事。當然,到底在那里做,還是要決定的。最終選擇在PC 上,畢竟PC上調試起來容易。(天煞的,這個POS機沒有debug,也沒有模擬器,顯示屏還沒我手機的大,做起來著實費事)。
                其實,我的本意是想在POS上做這個轉換。因為POS用的是C(一個不知什么年代的gcc),可以直接操作字節。基本的代碼看起來差不多應該是這樣:
                    unsigned long InvData(unsigned long val, int n) {
                        unsigned long t=val, res=0;
                        for(; n >0; n--)
                        {
                            res = res << 8;
                            res |= (unsigned char)t;
                            t = t >> 8;
                        }
                        return res;
                    }
                n是數據類型的字節長度。這里使用了最長的無符號整數類型。這是核心轉換函數,各種類型的轉換函數都可以從中派生:
                    long InvDataLong(long val) {
                        return (long)InvData((unsigned long)val, sizeof(val));
                    }
                    short InvDataShort(short val) {
                        return (short)InvData((unsigned short)val, sizeof(val));
                    }
                    ...
                最后,有一個比較特殊的地方,float。float的編碼不同于整型,如果直接用(unsigned long)強制類型轉換,只會把float數值的整數部分賦予參數,得不到正確的結果。正確的做法,應當是把float占用的四個字節直接映射成一個 unsigned long:
                    float InvDataFloat(float val) {
                        float val=InvData(*(unsigned long*)(&val), sizeof(val));
                        return *(float*)(&val);
                    }
                通過將float變量的地址強制轉換成unsigned long*類型,然后再dereference成unsigned long類型。當然還有其他辦法,比如memcpy,這里就不多說了。至于double類型,為了簡化問題,這里將其忽略。如果有64位的整型,那么 double可以采用類似的解法。否則,就必須寫專門的處理函數。
                當然,最終我還是使用C#寫這個轉換。相比之下,C#的轉換代碼顯得更具現代風味。基本算法還是一樣:
                    public static ulong DataInv(ulong val, int n)
                    {
                        ulong v1_ = val, v2_ = 0;

                        for (; n > 0; n--)
                        {
                            v2_ <<= 8;
                            v2_ |= (byte)v1_;
                            v1_ >>= 8;
                        }

                        return v2_;
                    }
                對于習慣于C/C++的同學們注意了,long/ulong在C#中不是4字節,而是8字節。也就是C/C++中的longlong。以這個函數為基礎,其它整數類型的字節序轉換也就有了:
                    public static ulong DataInv(ulong val)
                    {
                        return DataInv(val, sizeof(ulong));
                    }

                    public static uint DataInv(uint val)
                    {
                        return (uint)DataInv((ulong)val, sizeof(uint));
                    }

                    public static int DataInv(int val)
                    {
                        return (int)DataInv((uint)val);
                    }
                    ...
                然而,面對float,出現了麻煩。在C#中,沒有指針,無法象C那樣將float展開成ulong。(unsafe代碼可以執行這類操作,但這不是C#嫡親的特性,并且是違背C#設計理念的。這里不做考慮)。C#提供了另一種風格的操作:
                    public static float DataInv(float val)
                    {
                        float res_ = 0;

                        byte[] buf_ = BitConverter.GetBytes(val);
                        byte t = 0;

                        t = buf_[0];
                        buf_[0] = buf_[3];
                        buf_[3] = t;

                        t = buf_[1];
                        buf_[1] = buf_[2];
                        buf_[2] = t;

                        res_ = BitConverter.ToSingle(buf_, 0);

                        return res_;
                    }
                這個做法盡管有些累贅,但道理上很簡單:把float變量轉換成一個字節流,然后把相應的位置對調,就獲得了字節反序的float。相比C的float轉 換,C#明顯不夠簡練。原因很簡單,C#根本不是用來干這個的。C是一種非常底層的語言,它的內存模型是完全直觀的,與硬件系統相對應的。因而,對于這種 與機器相關的操作,當然也顯得游刃有余。而C#定位于高層開發的高級語言,底層的內存模型是被屏蔽的,程序員無需知道和關心。
                不過,C#的代碼卻擁有它的優勢。只需看一眼這些函數的使用代碼,便不言自明了:
                    //C代碼
                    int x=234;
                    float y=789.89;
                    short z=332;
                    x=InvDataInt(x);
                    y=InvDataFloat(y);
                    z=InvDataShort(z);

                    //C#代碼
                    int x=234;
                    float y=789.89;
                    short z=332;
                    x=DataInv(x);
                    y=DataInv(y);
                    z=DataInv(z);
                在C代碼中,對于不同的類型,需要使用不同命名的函數。而在C#代碼中,則只需使用DataInv這樣一個函數名。至于屆時選用那個版本的函數,編譯器會 根據實際的類型自動匹配。C#運用函數重載這個特性,使得調用代碼可以采用統一的形式。即便是數據的類型有所變化,也無需對調用代碼做任何修改。(這在我 的開發過程中的得到了驗證,傳輸數據的成員類型曾經發生變化,我也只是修改了數據結構的定義,便將問題搞定)。這一點,在C中是無法做到的。
                歸結起來,C由于側重于底層,在數據轉換方便的靈活性,使得轉換代碼的構建更加容易。而C#則得益于函數重載,在轉換代碼使用方面,有獨到的優勢。
                迄今為止,三只小豬中,還只有兩只出現。下面就要第三只出場了。
                作為C++的粉絲,我會自然而然地想到使用C++來實現這個轉換功能。于是便有了如下的代碼:
                   unsigned long InvData(unsigned long val, int n) {
                        unsigned long t=val, res=0;
                        for(; n >0; n--)
                        {
                            res = res << 8;
                            res |= (unsigned char)t;
                            t = t >> 8;
                        }
                    }
                    long InvData(long val) {
                        return (long)InvData((unsigned long)val, sizeof(val));
                    }
                    short InvData(short val) {
                        return (short)InvData((unsigned short)val, sizeof(val));
                    }
                    ...
                    float InvData(float val) {
                        float val=InvData(*(unsigned long*)(&val), sizeof(val));
                        return *(float*)(&val);
                    }
                這些代碼就好象是C和C#代碼的雜交后代。既有C的底層操作,也有C#的函數重載,兼有兩者的優點。
                不過,還能做得更好:
                    template<typename T>
                    T InvData(T val) {
                        T t=val, res=0;
                        int n=sizeof(T);
                        for(; n >0; n--)
                        {
                            res = res << 8;
                            res |= (unsigned char)t;
                            t = t >> 8;
                        }
                        return (T)res;
                    }
                這樣,就把所有的整型都一網打盡了,僅用一個函數模板,便完成了原先諸多函數所做的工作。而float版本的函數則保持不變,作為InvData()的一個重載。按照C++的函數模板-重載規則,float版的函數重載將被優先使用。

                好了,三只小豬的故事講完了。前兩只小豬各有優點,也各有缺點。而第三只小豬則雜合和前兩者的優點,并且具有更大的進步。盡管第三只小豬存在各種各樣的缺陷,但畢竟它的眾多特性為我們帶來了很多效率和方便,這些還是應當值得肯定的。

            附:第三只小豬的其他手段:
            1、強制轉換成字符串數組
            template<typename T>
            T InvData1(T v) {
                unsigned char* pVal1=(unsigned char*)(&v)
                    , *pVal2=pVal1+sizeof(T)-1, t;
                while(pVal2-pVal1>1)
                {
                    t=*pVal2;
                    *pVal2=*pVal1;
                    *pVal1=t;
                    pVal1++;
                    pVal2--;
                }
                return v;
            }
            2、使用標準庫,blog上有人留言建議的
            template<typename T>
            T InvData(T v) {
                unsigned char* pVal=(unsigned char*)(&v);
                size_t n=sizeof(T);
                std::reverse(pVal, pVal+n, pVal);
            }
            3、使用traits
            template<size_t n> struct SameSizeInt;
            template<> struct SameSizeInt<1> { typedef unsigned char Type; };
            template<> struct SameSizeInt<2> { typedef unsigned short Type; };
            template<> struct SameSizeInt<4> { typedef unsigned long Type; };
            template<> struct SameSizeInt<8> { typedef unsigned longlong Type; };

            template<typename T>
            T InvData(T v) {
                size_t n=sizeof(T);
                typedef SameSizeInt<sizeof(T)>::Type NewT;
                NewT v1=*(NewT*)(&v), v2=0;
                for(; n >0; n--)
                {
                    v2= v2<< 8;
                    v2|= (unsigned char)v1;
                    v1 = v1 >> 8;
                }
                return *(T*)(&v2);
            }

            甚至可以使用tmp去掉其中的循環。在C++中,這類任務的實現方法,完全看程序員的想象力了。:)
            posted @ 2008-09-18 19:25 longshanks 閱讀(1935) | 評論 (3)編輯 收藏

            2008年8月2日 #

            GP技術的展望——C--
            莫華楓

                C++的復雜是公認的,盡管我認為在人類的聰明智慧之下,這點復雜壓根兒算不上什么。不過我得承認,對于一般的應用而言,C++對程序員產生的壓力還是不 小的。畢竟現在有更多更合適的選擇。僅僅承認復雜,這沒有什么意義。我不時地產生一個念頭:有什么辦法既保留C++的優點,而消除它的缺點和復雜。我知道 D語言在做這樣的事情。但是,D更多地是在就事論事地消除C++的缺陷,而沒有在根本上消除缺陷和復雜性。
                一般而言,一樣東西復雜了,基本上都是因為東西太多。很顯然,C++的語言特性在眾多語言中是數一數二的。于是,我便想到或許把C++變成“C--”,可以治好C++的復雜之病。在探討這個問題之前,讓我們先從C出發,看看C++為何如此復雜。

            C和C++

                盡管存在這樣那樣的不足,比如non-lalr的語法、隱式的指針類型轉換等,但C語言的設計哲學卻是足夠經典的。C語言有一個非正式的分類,認為它既非匯編這樣的低級語言,也非Pascal那樣的高級語言, 而應該算作中級語言,介于其他兩類語言之間。這種分類恰如其分地點出了C語言的特點和理念:以高級語言語法形式承載了低級語言的編程模型。低級語言的特點 是可以直接地描述硬件系統的結構。C則繼承了這個特點。C語言直觀地反映了硬件的邏輯構造,比如數組是內存塊,可以等價于指針。在C語言中,我們可以幾乎 直接看到硬件的構造,并且加以操作。這些特性對于底層開發至關重要。
                然而,C的這種直觀簡潔的模型過于底層和瑣碎,不利于應用在那些構造復雜、變化多樣的應用中。針對C的這些弱點,Bjarne Stroustrup決心利用OOP技術對C語言進行改造,從而促使了C++的誕生。C++全面(幾乎100%)地兼容C,試圖以此在不損失C語言的直觀 和簡潔的情況下,同時具備更強的軟件工程特性,使其具備開發大型復雜系統的優勢。這個目標“幾乎”達到了,但是代價頗為可觀。
                在經歷了80、90年代的輝煌之后,C++的應用領域開始退步。一方面,在底層應用方面,C++的很多特性被認為是多余的。如果不使用這些特性,那么 C++則同C沒有什么差別。相反這些特性的使用,對開發團隊的整體能力提出了更高的要求。因而,在最底層,很多人放棄了C++而回歸了C,因為那些高級特 性并未帶來很多幫助,反而產生了很多負擔。另一方面,在高層開發中,業務邏輯和界面也無需那么多底層的特性和苛刻的性能要求,更多簡單方便、上手容易的語 言相比C++更加適合。C++的應用被壓縮在中間層,隨著業界系統級開發的不斷專業化,C++的使用規模也會越來越小。(當然,它所開發的應用往往都是關 鍵性的,并且是沒有選擇余地的)。實際上,C++在這個層面也并非完美的工具。目前無法取代是因為沒有相同級別的替代品。D或許是個強有力的競爭者,但一 方面出于歷史遺留代碼的規模和應用慣性,另一方面D也并未完全解決C++面臨的復雜性問題,D也很難在可見的將來取代C++。
                實際上,C++的這種尷尬地位有著更深層次的原因。C++的本意是在保留C的底層特性基礎上,增加更好的軟件工程特性。但是,C++事實上并未真正意義上 地保留C的底層特性。回顧一下C的設計理念——直觀而簡潔地反映底層硬件的特性。C++通過兼容C獲得了這種能力。但是這里有個問題,如果我要獲得C的這 種簡單直觀性,就必須放棄C++中的很多高級特性。這里最明顯的一個例子便是pod(plain old data)。
                在C中壓根沒有pod的概念,因為所有的對象都是pod。但是,C++中有了pod。因為C++的對象可能不是一個pod,那么我們便無法象在C當中那樣 獲得直觀簡潔的內存模型。對于pod,我們可以通過對象的基地址和數據成員的偏移量獲得數據成員的地址,或者反過來。但在非pod對象中,卻無法這么做。 因為C++的標準并未對非pod對象的內存布局作出定義,因而對于不同的編譯器,對象布局是不同的。而在C中,僅僅會因為底層硬件系統的差異而影響對象布 局。
                這個問題通常并不顯而易見。但在很多情況下為我們制造了不小的障礙。比如,對象的序列化:我們試圖將一個對象以二進制流的形式保存在磁盤中、數據庫中,或 者在網上傳輸,如果是pod,則直接將對象從基地址開始,按對象的大小復制出來,或傳輸,或存儲,非常方便。但如果是非pod,由于對象的不同部分可能存 在于不同的地方,因而無法直接復制,只能通過手工加入序列化操作代碼,侵入式地讀取對象數據。(這個問題不僅僅存在于C++,其他語言,如java、C# 等都存在。只是它們沒有很強烈的性能要求,可以使用諸如reflect等手段加以處理)。同樣的問題也存在于諸如hash值的計算等方面。這對很多開發工 作造成不小的影響,不僅僅在底層,也包括很多高層的應用。
                究其原因,C++僅僅試圖通過機械地將C的底層特性和OOP等高層特性混合在一起,意圖達到兩方兼顧的目的。但是,事與愿違,OOP的 引入實際上使得C的編程模型和其他更高級的抽象模型無法兼容。在使用C++的過程中,要么只使用C的特性,而無法獲得代碼抽象和安全性方面的好處,要么放 棄C的直觀簡潔,而獲得高層次的抽象能力。反而,由于C和OOP編程模型之間的矛盾,大大增加了語言的復雜性和缺陷數。

            舍棄

                但是,我們可以看到在C++中,并非所有的高級特性都與C的底層特性相沖突。很多使用C而不喜歡C++的人都表示過他們原意接受OB,也就是僅僅使用封裝 。對于RAII,基本上也持肯定的態度。或許也會接受繼承,但也表露出對這種技術帶來的復雜性的擔心。動多態是明顯受到排斥的技術。顯然這是因為動多態破壞了C的編程模型,使得很多本來簡單的問題復雜化。不是它不好,或者沒用,是它打破了太多的東西。
                因而,我們設想一下,如果我們去除動多態特性,那么是否會消除這類問題呢?我們一步步看。
                動多態的一個基本支撐技術是虛函數。在使用虛函數的情況下,類的每一次繼承都會產生一個虛函數表(vtable),其中存放的是指向虛函數的指針。這些虛函數表必須存放在對象體中,也就是和對象的數據存放在一起(至少要關聯在一起)。因而,對象在內存里并不是以連續的方式存放,而被分割成不同的部分,甚至身首異處(詳見《Inside C++ Object Model》)。這便造成了前面所說的非pod麻煩。一旦放棄虛函數和vtable,對象的內存布局中,便不會有東西將對象分割開。所有的對象的數據存儲都是連續的,因而都是pod。在這一點上,通過去除vtable,使得語言回歸了C的直觀和簡單。
                動多態的內容當然不僅僅是一個虛函數,另一個重要的基石是繼承。當然,我們并不打算放棄繼承,因為它并不直接破壞C的直觀性和簡潔性。不同于虛函數,繼承 不是完全為了動多態而生的。繼承最初的用途在于代碼復用。當它被賦予了多態含義后,才會成為動多態的基礎。以下的代碼可以有兩種不同的解讀:
                class B : public A {};
                從代碼復用的角度來看,B繼承自A,表示我打算讓B復用A的所有代碼,并且增加其他功能。而從多態的角度來看,B是一個A的擴展,B和A之間存在is-a的 關系。(B是一個A)。兩者是站在不同層面看待同一個問題。代碼復用,代表了編碼的觀點,而多態,則代表了業務邏輯的觀點。但是,兩者并非實質上的一回 事。在很多情況下,基類往往作為繼承類的某種代表,或者接口,這在編碼角度來看并沒有對等的理解。而這種接口作用,則是動多態的基礎。動多態通過不同的類 繼承自同一個基類,使它們擁有共同的接口,從而可以使用統一的形式加以操作。作為一個極端,interface(或者說抽象基類),僅僅擁有接口函數(即vtable)而不包含任何數據成員。這是純粹的接口。
                然而,這里存在一個缺陷。一個接口所代表的是一組類,它將成為這一組類同外界交互的共同界面。但是,使用基類、或者抽象基類作為接口,實質上是在使用一個 類型來代表一組類型。打個比方,一群人湊在一起出去旅游,我們稱他們這群人為“旅行團”。我們知道旅行團不是一個人,而是一個不同于“人”的概念。動多態 里的接口相當于把一個旅行團當作一個人來看待。盡管這只是邏輯上的,或許一個旅行團的很多行為和一個人頗為相似。但是根本上而言,兩者畢竟不是相同層次的 概念。這樣的處理方法往往會帶來了很多弊端。
                為了使繼承被賦予的這重作用發揮作用,還需要一項非常關鍵的處理:類型轉換。請看以下代碼:
                void func(A* a);
                B b;
                func(&b);
                最后這行代碼施行了動多態,如果B override了A的虛函數的話。很顯然,如果嚴格地從強類型角度而言,&b是不應當作為func的實參,因為兩者類型不匹配。但是如果拒絕接 受&b作為實參,那么動多態將無法進行下去。因此,我們放寬了類型轉換的限制:允許繼承類對象的引用或指針隱式地轉換成基類的引用或指針。這樣, 形如func(&b);便可以順理成章地成為合法的代碼。
                然而,這也是有代價的:
                B ba[5];
                func(ba);
                后面這行函數調用實際上是一個極其危險的錯誤。假設在func()中,將形參a作為一個類型A的數組對待,那么當我們使用ba作為實參調用func()的 時候,會將ba作為A的 數組處理。我們知道,數組內部元素是緊挨著的,第二個元素的位置是第一個元素的基址加上元素的尺寸,以此類推。如果傳遞進來的對象數組是B類型的,而被作 為A類型處理,那么兩者的元素位置將可能不同步。盡管B繼承自A,但是B的尺寸很有可能大于A,那么從第二個元素起,a[1]的地址并非ba[1]的地 址。于是,當我們以a[1]訪問ba時,實際上很可能在ba[0]的內部某個位置讀取,而func()的代碼還以為是在操作ba[1]。這便是C++中的 一個重要的陷阱——對象切割。這種錯誤相當隱蔽,危險性極大。
                由于C++試圖保留C的編程模型,因而保留了指針-數組的等價性。這種等價性體現了數組的本質。這在C中是一項利器,并無任何問題。但在C++中,由于存 在了繼承,以及繼承類的隱式類型轉換,使得這種原本滋補的特性成為了一劑毒藥。換句話說,C++所引入的動多態破壞了C的直觀性。

            舍棄之后

                從上面的分析來看,動多態同C的編程模型是不相容的。因而如果希望得到C的直觀性,并且消除C++的缺陷,必須放棄動多態這個特性。現在來看看放棄之后將會怎樣。
                一旦放棄了動多態,也就放棄了虛函數和vtable。此時,所有的對象都是pod了。那么首當其沖的好處,就是可以進行非侵入的序列化、hash計算等等 操作。由于對象肯定是連續分布的,可以直接地將對象取出進行編碼、存儲、計算和傳輸,而無需了解對象內部的數據結構和含義。另外一個重要的問題也會得到解 決,這就是ABI。在C中統一的ABI很自然地存在于語言中。我們可以很容易地用link將兩個不同編譯器編譯的模塊連接起來,而不會發生問題。但 是,C++中做不到,除非不再使用類而使用純C。目前C++還沒有統一的ABI,即便標準委員會有意建立這樣的規范,實現起來也絕非易事。但是,如果放棄 動多態之后,對象的布局便回歸到C的形態,從而使得ABI不再成為一個問題。
                另一方面,隨著動多態的取消,那么繼承的作用被僅僅局限于代碼復用,不再具有構造接口的作用。我們前面已經看到,繼承類向基類的隱式轉換,是為了使基類能 夠順利地成為繼承類的接口。既然放棄了動多態,那么也就無需基類再承擔接口的任務。那么由繼承類向基類的隱式類型轉換也可以被禁止:
                void func(A* a);
                B b;
                func(&b);  //編譯錯誤,類型不匹配
                進而對象切割也不會發生:
                B ba[5];
                func(ba); //編譯錯誤,類型不匹配
                盡管根據數組-指針的等價性,ba可以被隱式地轉換為B*,但是B*不再能夠隱式地轉換為A*,從而避免了對象的切割。
                問題是,如此簡單地將動多態放棄掉,就如同將水和孩子一起潑掉那樣,實際上放棄了動多態帶來的好處。實際上并非如此。我們放棄動多態這個特性,但并不打算放棄它所具有的功能,而是用另一種技術加以替代。這便是runtime concept(這里這里)。
                不同于以類型為基礎的interface,concept是獨立于類型的系統。concept生來便是為了描述一組類型,因而是接口最理想的實現手段。當concept runtime化之后,便具有了與動多態相同的功能(很多方面還有所超越)。
                runtime concept同樣需要類似vtable的函數分派表,但由于它不是類型,這些分派表無需存放在對象內部,可以獨立放置(可以同RTTI信息放在一起), 并且只需一份。正是基于這個特性,方才保證了所有對象依然是pod,依然能夠保證對象布局的直觀性。
                同樣,runtime concept承擔了接口的任務,但不象動多態那樣依賴于繼承和相應的隱式類型轉換。(通過自動或手動的concept_map)。因而,我們依舊可以禁止基于繼承關系的隱式類型轉換,從而防止對象切割的情況。
                一旦使用concept作為多態的實現手段,反倒促使原本動多態的一些麻煩得到消除。在動多態中,必須指定virtual函數。如此,在一個類中會存在兩 種不同形態的函數,實現動多態的虛函數,和無此功能的普通函數。準確地維護這樣兩種函數,頗有些難度。而且,函數是虛還是不虛,牽涉到系統的設計,必須在 最初構建時確定,否則以后很難修改。但在放棄動多態,使用concept的情況下,只要一個繼承類中,使用相同的簽名覆蓋基類中的函數,便實現了多態。當 進行concept_map,即將接口與類綁定時,只會考慮繼承類的函數,而忽略基類中被覆蓋的函數。于是,只需簡單的覆蓋,便實現了多態的控制。對于是 否多態一個函數,即是否改變基類函數的行為,完全由繼承類控制,在創建基類時不必為此傷神。其結果就是,我們無需在系統設計的最初一刻就操心多態的問題, 而只需根據實現的需要隨時實現。

            其他

                存在大量隱式轉換也是C++常受人詬病的一個方面,(特別是那些Pascal系的程序員)。隱式轉換的目的是帶來方便,使得編碼更加簡潔,減少冗余。同時也使得一些技巧得以施行。但是,隱式轉換的副作用也頗為可觀。比如:
                void fun(short a);
                long a=1248;
                fun(a); //頂多一個警告
                這種轉換存在兩面性:一方面,它可能是合理的,因為盡管a類型long大于short,但很可能存放著short可容納的數值;但另一方面,a的確存在short無法容納的可能性,這便會造成一個非常隱蔽的bug。
                C/C++對此的策略是把問題扔給程序員處理,如果有bug那是程序員的問題。這也算得上合情合理,畢竟有所得必有所失,也符合C/C++的一貫理念。但 終究不是最理想的方式。但是如果象Pascal那樣將類型管得很死,那么語言又會失去靈活性,使得開發的復雜性增加。
                如果試圖禁止隱式類型轉換,那么為了維持函數使用代碼的簡潔性,函數必須對所有的類型執行重載。這大大增加了函數實現的負擔,并且重復的代碼嚴重違背了DRY原則。
                現在或許存在一些途徑,使得在維持絕對強類型的情況下獲得所希望的靈活性。鑰匙可能依然在concept手上。考慮如下的代碼:
                void fun(Integers a);
                long a=1248;
                fun(a);
                longlong b=7243218743012;
                fun(b);
                此處,fun()是一個函數,它的形參是一個concept,代表了所有的整型。這樣,這個函數便可以接受任何一種整型(或者具有整型行為的類型)。我們 相信,在一般的應用下,任何整數都有完全相同的行為。因此,我們便可以很方便地使用Integers這個接口執行對整數的操作,而無需關心到底是什么樣的 整數。
                如此,我們便可以在禁止隱式類型轉換,不使用函數重載的情況下,完成這種函數的編寫。同時可以得到更好的類型安全性。

                強制類型轉換是非常重要的特性,特別是在底層開發時。但也是雙刃劍,往往引來很隱蔽的錯誤。強制類型轉換很多情況下是無理的,通常都是軟件的設計問題造成的。但終究還是有一些情況,需要它來處理。
                設想這樣一個場景:兩個一模一樣的類型,但它們分屬不同的函數。(這種情形盡管不多見,但還是存在的。這往往是混亂設計的結果。當然也有合理的情況,比如 來自兩個不同庫的類型)。我現在需要寫一個函數,能夠同時使用這兩個類型。比較安全一些的,可以用函數重載。但是兩個重載的函數代碼是一樣的,典型的冗余 代碼。當然也可以針對其中一個結構編寫代碼,然后在使用時,對另一個結構的實例執行強制類型轉換。但是,強制類型轉換畢竟不是件好事。因此,我們也可以構 造一個concept,讓它描述這兩個類型。然后在編寫函數時使用這個concept,當這兩個類型都與concept綁定后,便可以直接使用這兩個類 型,而沒有類型安全和代碼冗余的問題。
                (順便提一下,這種方式也可以運用在類型不同的情況下。比如兩個類型不完全相同,但是基本要素都一樣。那么就可以使用concept_map的適配功能, 將兩個類型統一在一個concept下。這種方式相比oop的Adapter模式,更加簡潔。adapter本身是一個container,它所實現的接 口函數,都必須一一轉發到內部的對象,編寫起來相當繁瑣。但在concept_map中,對于那些符合concept描述的函數無需另行處 理,concept會自動匹配,只需對那些不符合要求的函數執行適配。)

                前面說過,指針數組的等價性體現了一種直觀的編程模型。但是,指針和數組畢竟還是存在很多差別,比如指針僅僅表達了一組對象在內存中的位置,但并未攜帶對象數量的信息。因而,當數組退化成指針時,便已經失去了數組的身份:
                void func(int* x);
                int a[20];
                func(a);
                這里,在函數func中已經無法將a作為數組處理,因為無法知道變成int*后的a有多大來避免越界。甚至我們無法把a作為多個對象構成的內存塊看待,因為我們不知道大小。因此,只有顯式地給出數組大小,才能使用:
                void func(int* x, long size);
                但是,在concept的作用下,數組和指針得以依然保持它們的等價性的情況下,解決數組退化問題。考慮這樣兩個函數:
                void func1(Pointer x);
                void func2(Container x);
                其中,Pointer是代表指針的concept,而Container則是代表容器的concept。必須注意的是,Pointer是嚴格意義上的指 針,也就是說無法在Pointer上執行迭代操作。Pointer只能作為指針使用,只具備dereference的能力(很像java的“指針”,不是 嗎?concept在沒有放棄C的底層特性的情況下也做到了。)。而Container則是專門用來表達容器的concept,其基本的特性便是迭代。在 func1中,無法對形參x執行迭代,僅僅將其作為指向一個對象的指針處理,保證其安全性。而對于需要進行迭代操作的func2而言,x則是可以遍歷的。 于是,對于同一個數組a,兩個函數分別從不同的角度對其進行處理:
                int a[20];
                func1(a); //a直接作為指針處理,但不能迭代
                func2(a); //a作為容器處理,可以迭代,并且其尺寸信息也一同傳入
                此處實際上是利用了concept對類型特性的描述作用,將具有兩重性的數組類型(數組a即代表了數組這個容器,也代表了數組的起始地址)以不同特征加以 表達,以滿足不同應用的需求。數組仍然可以退化成指針,C的直觀模型得到保留,在很多特殊的場合發揮作用。但在其他應用場景,可以更加安全地使用數組。
               

            總結

                綜上所述,C++未能真正延續C的直觀簡潔,主要是由于動多態的一些基礎設施破壞了C的編程模型。因而,我們可以通過放棄動多態,及其相關的一些技術,代 之以更加“和諧”的runtime concept,使得C++在基本保留C的編程模型的同時,獲得了相比原來更好的軟件工程特性。至此,這種改變后的C++(如果還能稱為C++的話)擁有 如下的主干特性:
                1、SP,來自于C。
                2、完全pod化。
                3、OB。保留了封裝和RAII。盡管也保留了繼承,但其作用僅限于代碼復用,禁止基于繼承的隱式類型轉換。
                4、GP,包括static和runtime concept。這是抽象高級特性的核心和基石。
                這樣的語言特性實質上比現有的C++更加簡潔,但是其能力更加強大。也比C++更易于貼近C的編程模型,以便適應底層的開發。我不能說這樣的變化是否會產生一個更好的語言,但是我相信這些特性有助于構造更加均衡統一的語言。
            posted @ 2008-08-02 20:57 longshanks 閱讀(3001) | 評論 (14)編輯 收藏

            2008年7月26日 #

            GP技術的展望——先有鴻鈞后有天

            莫華楓


                自從高級語言出現以來,類型始終是語言的核心。幾乎所有語言特性都要以類型作為先決條件。類型猶如天地,先于萬物而存在。但是,是否還有什么東西比類型更加原始,更加本質,而先于它存在呢?請往下看。:)

            泛型和類型

                泛型最簡短最直觀的描述恐怕應該是:the class of type。盡管這樣的描述不算最完備,但也足以說明問題。早在60年代,泛型的概念便已經出現。最初以“參數化類型”的名義存在。70年代末期發展起來的 恐龍級的Ada(我的意思不是說Augusta Ada Byron Lovelace伯爵夫人是恐龍,從畫像上看,這位程序員的祖師奶長得相當漂亮:)),尚未擁有oop(Ada83),便已經實現了泛型(Generic)。盡管泛型歷史悠久,但真正全面地發展起來,還是在90年代初, 天才的Alexander A. Stepanov創建了stl,促使了“泛型編程”(Generic Programming)的確立。
                出于簡便的目的,我套用一個老掉牙的“通用容器”來解釋泛型的概念。(就算我敷衍吧:P,畢竟重頭戲在后面,具體的請看前面給出的鏈接)。假設我在編程時需要一個int類型的棧,于是我做了一個類實現這個棧:
                class IntStack {...};
                用的很好。過了兩天,我又需要一個棧,但是類型變成了double。于是,我再另寫一個:
                class DoubleStack {...};
                不得了,好象是通了馬蜂窩,不斷地出現了各種類型的棧的需求,有string的,有datetime的,有point的,甚至還有一個Dialog的。每 種類型都得寫一個類,而且每次代碼幾乎一樣,只是所涉及的類型不同而已。于是,我就熱切地期望出現一種東西,它只是一個代碼的框架,實現了stack的所 有功能,只是把類型空著。等哪天我需要了,把新的類型填進去,便得到一個新的stack類。
                這便是泛型。
                但是,僅僅這些,還不足以成就GP的威名。
                我有一個古怪的需求(呵呵,繼續敷衍。:)):
                做一個模板,內部有一個vector<>成員:
                template<typename T> A
                {
                    ...
                    vector<T> m_x;
                };
                可是,如果類型實參是int類型的話,就得用set<>。為了使用的方便,模板名還得是A。于是,我們就得使用下面的技巧:
                template<> A<int>
                {
                    ...
                    set<T> m_x;
                };
                這叫特化(specialization),相當于告訴編譯器如果類型實參是int,用后面那個。否則,用前面的。特化實際上就是根據類型實參由編譯器執行模板的選擇。換句話說,特化是一種編譯期分派技術。
                這里還有另一個更古怪需求:如果類型實參是指針的話,就用list<>。這就得用到另一種特化了:
                template<typename T> A<T*>
                {
                    ...
                    list<T> m_x;
                }
                這是局部特化(partial specialization),而前面的那種叫做顯式特化(explicit specialization),也叫全特化。局部特化則是根據類型實參的特征(或者分類)執行的模板選擇。
                最后,還有一個最古怪的需求:如果類型實參擁有形如void func(int a)成員函數的類型,那么就使用deque。這個...,有點難。現有的C++編譯器,是無法滿足這個要求的。不過希望還是有的,在未來的新版C++09中,我們便可以解決這個問題。

            Concept和類型

                concept是GP發展必然結果。正如前面所提到的需求,我們有時候會需要編譯器能夠鑒識出類型的某些特征,比如擁有特定的成員等等,然后執行某種操作。下面是一個最常用的例子:
                swap()是一個非常有用的函數模板,它可以交換兩個對象的內容,這是swap手法的基礎。swap()的基本定義差不多是這樣:
                template<typename T> swap(T& lhs, T& rhs) {
                    T tmp(lhs);
                    lhs=rhs;
                    rhs=tmp;
                }
                但是,如果需要交換的對象是容器之類的大型對象,那么這個swap()的性能會很差。因為它執行了三次復制,這往往是O(n)的。標準容器都提供了一個 swap成員函數,通過交換容器內指向數據緩沖的指針,獲得O(1)的性能。因此,swap()成員是首選使用的。但是,這就需要程序員識別對象是否存在 swap成員,然后加以調用。如果swap()函數能夠自動識別對象是否存在swap成員,那么就可以方便很多。如果有swap成員,就調用成員,否則, 就是用上述通過中間變量交換的版本。
                這就需要用到concept技術了:
                template<Swappable T> void swap(T& lhs, T& rhs) {
                    lhs.swap(rhs);
                }
                這里,Swappable是一個concept:
                concept Swappable<typename T> {
                    void T::swap(T&);
                }
                于是,如果遇到擁有swap成員函數的對象,正好符合Swappable concept,編譯器可以使用第二個版本,在O(1)復雜度內完成交換。否則,便使用前一個版本:
                vector a, b;
                ... //初始化a和b
                swap(a,b); //使用后一個版本
                int c=10, d=23;
                swap(c, d); //使用前一個版本
                這里的swap()也是運用了特化,所不同的是在concept的指導下進行的。這樣的特化有時也被稱作concept based overload。
                從上面的例子中可以看到,原先的特化,無論是全特化,還是局部特化,要么特化一個類型,要么特化一個大類(如指針)的類型。但無法做到更加精細。比如,我 希望一個模板能夠針對所有的整數(int,long,short,char等)進行特化,這在原先是無法做到的。但擁有了concept之后,我們便可以 定義一個代表所有整數的concept,然后使用這個整數concept執行特化。換句話說,concept使得特化更加精細了,整個泛型系統從原來“離 散”的變成了“連續”的。
                不過上面那個concept特化的模板看起來實在不算好看,頭上那一坨template...實在有礙觀瞻。既然是concept based overload,那么何不直接使用重載的形式,而不必再帶上累贅的template<...>:
                void fun(anytype a){...} //#1,anytype是偽造的關鍵字,表示所有類型。這東西最好少用。
                void fun(Integers a){...} //#2,Integers是concept,表示所有整型類型
                void fun(Floats a){...} //#3,Floats是concept,表示所有浮點類型
                void fun(long a){...} //#4
                void fun(int a){...} //#5
                void fun(double a){...} //#6
                ...
                int x=1;
                long y=10;
                short z=7;
                string s="aaa";
                float t=23.4;
                fun(x); //選擇#5
                fun(y); //選擇#4
                fun(z); //選擇#2
                fun(s); //選擇#1
                fun(t); //選擇#3
                這種形式在語義上與原來的模板形式幾乎一樣。注意,是幾乎。如下的情形是重載形式無法做到的:
                template<Integers T> T swap(T lhs, T rhs) {
                    T temp(lhs);
                    ...
                }
                這里,模板做到了兩件事:其一,模板萃取出類型T,在函數體中,可以使用T執行一些操作,比如上述代碼中的臨時對象temp的構造。這個問題容易解決,因為萃取類型T還有其他的方法,一個typeof()操作符便可實現:
                Integers swap(Integers lhs, Integers rhs) {
                    typeof(lhs) temp(lhs);
                    ...
                }
                其二,模板保證了lhs,rhs和返回值都是同一類型。這個問題,可以通過施加在函數上的concept約束解決:
                Integers swap(Integers lhs, Integers rhs)
                    requires SameType<lhs, rhs>
                        && SameType<lhs, retval> {  //retval是杜撰的關鍵字,用以表示返回值
                    typeof(lhs) temp(lhs);
                    ...
                }
                相比之下,重載形式比較繁瑣。總體而言,盡管重載形式冗長一些,但含義更加明確,更加直觀。并且在concept的接口功能作用下,對參數類型一致的要求 通常并不多見(一般在基本類型,如整型等,的運算處理中較多見。因為這些操作要求類型有特定的長度,以免溢出。其他類型,特別是用戶定義類型,通常由于封 裝的作用,不會對類型的內部特性有過多要求,否則就不應使用泛型算法)。如果可以改變語法的話,那么就能用諸如@代替typeof,==代替 SameType的方法減少代碼量:
                Integers swap(Integers lhs, Integers rhs)
                    requires @lhs == @rhs && @lhs == @retval {
                    @lhs temp(lhs);
                    ...
                }
               

            Concept、類型和對象

                事情還可以有更加夸張的發展。前面對泛型進行了特化,能不能對類型也來一番“特化”呢?當然可以:
                void fun(int a);
                void fun(int a:a==0); //對于類型int而言,a==0便是“特化”了
                更完整的,也可以有“局部特化”:
                void fun(int a); //#1
                void fun(int a:a==0); //#2
                void fun(int a:a>200); //#3
                void fun(int a:a<20&&a>10); //#4
                void fun(int a:(a>70&&a<90)||(a<-10)); //#5
                ...
                int a=0, b=15, c=250, d=-50;
                fun(80); //使用#5
                fun(50); //使用#1
                fun(a); //使用#2
                fun(b); //使用#4
                fun(c); //使用#3
                fun(d); //使用#5
                實際上,這無非是在參數聲明之后加上一組約束條件,用以表明該版本函數的選擇條件。沒有約束的函數版本在沒有任何約束條件匹配的情況下被選擇。對于使用立 即數或者靜態對象的調用而言,函數的選擇在編譯期執行,編譯器根據條件直接調用匹配的版本。對于變量作為實參的調用而言,則需要展開,編譯器將自動生成如 下代碼:
                //首先將函數重新命名,賦予唯一的名稱
                void fun_1(int a); //#1
                void fun_2(int a); //#2
                void fun_3(int a); //#3
                void fun_4(int a); //#4
                void fun_5(int a); //#5
                //然后構造分派函數
                void fun_d(int a) {
                    if(a==0)
                        fun_2(a);
                    else if(a>200)
                        fun_3(a);
                    ...
                    else
                        fun_1(a);
                }
                在某些情況下,可能需要對一個對象的成員做出約束,此時便可以采用這種形式:
                struct A
                {
                    float x;
                };
                ...
                void fun(A a:a.x>39.7);
                ...
                這種施加在類型上的所謂“特化”實際上只是一種語法糖,只是由編譯器自動生成了分派函數而已。這個機制在Haskell等語言中早已存在,并且在使用上帶 來很大的靈活性。如果沒有這種機制,那么一旦需要增加函數分派條件,那么必須手工修改分派函數。如果這些函數,包括分派函數,是第三方提供的代碼,那么修 改將是很麻煩的事。而一旦擁有了這種機制,那么只需添加一個相應的函數重載即可。
                當concept-類型重載和類型-對象重載混合在一起時,便體現更大的作用:
                void fun(anytype a);
                void fun(Integers a);
                void fun(Floats a);
                void fun(long a);
                void fun(int a);
                void fun(double a);
                void fun(double a:a==0.8);
                void fun(short a:a<10);
                void fun(string a:a=="abc");
                ...
                concept-類型-對象重載體系遵循一個原則:優先選擇匹配的函數中最特化的。這實際上是類型重載規則的擴展。大的來說,所有類型比所屬的 concept更加特化,所有對象約束比所屬的類型更加特化。對于concept而言,如果concept A refine自concept B,那么A比B更加特化。同樣,如果一個類型的約束強于另一個,那么前一個就比后一個更加特化,比如a==20比a>10更加特化。綜合起來,可以 有這樣一個抽象的規則:兩個約束(concept,或者施加在對象上的約束)A和B,作用在類型或者對象上分別產生集合,如果A產生的集合是B產生的集合 的真子集,那么便認為A比B更加特化。
                根據這些規則,實際上可以對一個函數的重載構造出一個“特化樹”:

                越接近樹的根部,越泛化,越接近葉子,越特化。調用時使用的實參便在這棵“特化樹”上搜索,找到最匹配的函數版本。
                concept-類型-對象體系將泛型、類型和對象統一在一個系統中,使得函數的重載(特化)具有更簡單的形式和規則。并且,這個體系同樣可以很好地在類模板上使用,簡化模板的定義和使用。

            類模板

                C++的類模板特化形式并不惹人喜愛:
                template<typename T> A{...}; //基礎模板
                template<> A<int>{...}; //顯式特化(全特化)
                template<typename T> A<T*>{...}; //局部特化
                在C++09中,可以直接用concept定義模板的類型形參:
                template<Integers T> A{...};
                實質上,這種形式本身就是一種局部特化,因而原本那種累贅局部特化形式可以廢除,代之以concept風格的形式:
                template<Pointer T> A{...}; //Pointer表示此處采用指針特化模板
                同樣,如果推廣到全特化,形式也就進一步簡單了:
                template<int> A{...}; //這個形式有些突兀,這里只打算表達這個意思,應該有更“和諧”的形式
                如果模板參數是對象,則使用現有的定義形式:
                template<int a> A{...};
                更進一步,可以引入對象的約束:
                template<int a:a>10> A{...};
                此外,C++中在模板特化之前需要有基礎模板。但實際上這是多余的,D語言已經取消了這個限制,這對于簡化模板的使用有著莫大的幫助。

            從本質上講...

                從本質上講,我們可以把所有類型看作一個集合T={ti},而concept則是施加在類型集合上的約束。通過concept這個約束,我們便可以獲得類 型集合T的一個子集C。理論上,所有concept所對應的類型子集Cj構成了類型集合的冪集{Cj}。在{Cj}中,有兩類類型子集是很特殊的。一組是 T本 身,即所有類型。存在一個concept不對T施加任何約束,便得到了C0=T。第二類則是另一個極端,存在一組concept,施加在T上之后所得的類 型子集僅包含一個類型:Ci={ti}。由于這組concept與類型存在一一對應的關系,那么我們便可以用這組concept來指代類型。也就是把類型 作為特殊的concept處理。如此,concept便同類型統一在一個體系中。這種處理可以使我們獲得極大的好處。
                這組特殊的concept仍舊使用對應的類型名作為稱謂,仍舊稱之為“類型”,但其本質上還是concept。任何一個類型,一旦創建,也就創建了相應的特殊concept。如果在模板特化中使用一個類型的時候,實際上就是在使用相對應的那個特殊concept:
                void func(typeA a); //盡管使用了類型名typeA,但實際上這里所指的是typeA所對應的那個特殊concept。
                在這個concept體系的作用下,函數模板的特化和重載整個地統一起來(concept based overload)。
                至于作用在類型上的那種“特化”,也是同樣的道理。對于一個類型T而言,它所有的對象構成一個集合O。如果存在一組約束作用于O,那么每 一個約束對應著O的一個子集。理論上,我們可以構造出一組約束,使得他們同O的每一個子集一一對應。同樣,這些子集中有兩類子集比較特殊。一類是所有對象 的集合。另一類便是只有一個對象的子集。于是,我們可以使用這組特殊對象子集所對應的約束指代相應的對象。也就是將對象看作特殊的約束。如此,類型和對象 也被統一在一個系統中了。
                進而,類型在邏輯上被作為特殊concept處理,對象則被作為特殊的類型處理。于是,這三者便可以統一在一個體系下,一同參與特化。

            總結

                盡管形式不能代表本質,但形式的變化往往會帶來很多有益的進步。更重要的是,很多本質上的變化總會伴隨著形式上的改變。通過將concept、類型和對象 在邏輯上整合到統一的體系之中,便可以促使模板、特化、函數重載等機制在形式上達成統一。從而能夠簡化這些功能的使用。這也是當前重視語言(工具)易用性 的潮流的一個必然訴求。這個形式上的統一并非語法糖之類的表面變化。而是完全依賴于concept這個新型的類型描述(泛型)系統的確立和發展。 concept的出現,彌補了以往泛型的不足,找回了泛型系統缺失的環節,彌補了泛型同類型之間的裂痕。在此作用下,便可以構建起concept-類型- 對象的抽象體系,用統一的系統囊括這三個原本分立的概念。在這個新的三位一體的系統下,使得模板的特化和重載擁有了相同的形式,進而獲得更直觀的語義,和 更好的易用性。
            posted @ 2008-07-26 19:44 longshanks 閱讀(1917) | 評論 (10)編輯 收藏

            2008年2月26日 #

                 摘要: C++的營養 莫華楓     上一篇《C++的營養——RAII》中介紹了RAII,以及如何在C#中實現。這次介紹另一個重要的基礎技術——swap手法。 swap手法     swap手法不應當是C++獨有的技術,很多語言都可以實現,并且從中得到好處。只是C++存在的一些缺陷迫使大牛們發掘,并開始重視這種有用的手法。這 個原...  閱讀全文
            posted @ 2008-02-26 15:16 longshanks 閱讀(3890) | 評論 (3)編輯 收藏

            2008年2月16日 #

                 摘要: C++的營養 莫華楓     動物都會攝取食物,吸收其中的營養,用于自身生長和活動。然而,并非食物中所有的物質都能為動物所吸收。那些無法消化的物質,通過消化道的另一頭(某些動 物消化道只有一頭)排出體外。不過,一種動物無法消化的排泄物,是另一種動物(生物)的食物,后者可以從中攝取所需的營養。    一門編程語言,對于程序...  閱讀全文
            posted @ 2008-02-16 08:19 longshanks 閱讀(2054) | 評論 (2)編輯 收藏

            2008年2月14日 #

            瓦格納的排場

                這個春節過的實在無趣。走完親戚,招待完親戚,逛街買好東西,就沒多少時間了。看書的興致也沒了。想寫點什么,總是沒法集中精力。實在膩味了,把以前下載的瓦格納的歌劇《尼伯龍根指環》拿出來看看。自從下載,沒怎么好好看過,這回算是補上了。
                瓦格納的“指環”系列可以算是歌劇里的極品,總共四出:萊茵黃金、女武神、齊格佛雷德和眾神的黃昏。分成四個晚上連演,總共加起來大約15個小時。不說別 的,里面的角色眾多,光神就有8個,人有7個,女武神9個,尼伯龍根矮人2個,還有三個仙女、2個巨人,和一只小鳥(在后臺的女高音)。情節錯綜復雜,音 樂更是宏大。資料上說,這部歌劇中有200多個動機組合、交織在一起。不過,這還不能表現出瓦格納在音響上近乎變態的追求。指環系列要求一個超過100人 龐大的樂團,并且引入了幾種新的樂器,包括 Wagner tuba, bass trumpet和contrabass trombone。其中, Wagner tuba還是他為這部歌劇專門發明的。最夸張的是,瓦格納為了獲得如此龐大的樂隊同演員聲音之間的平衡,專門建造了一座歌劇院,也就是著名的拜羅伊特節日劇院(Bayreuth Festspielhaus)。這在音樂史上是絕無僅有的。正是由于這種駭人的排場,造就了歌劇史上的巔峰之作。
                這倒讓我聯想到C++。說實在的,C++的使用,有時也同瓦格納譜寫的歌劇那樣,非常復雜、龐大,需要大量的投入,和前期準備。有時,為了一些應用,而去構造一些的基礎設施(就像
            拜羅伊特節日劇院)。這種龐大導致了應用面的狹窄,但是卻能夠獲得極品般的東西。這種東西當然不會是到處都有,但卻是強大的、偉大的,以及關鍵性的。
                當然,排場僅僅是表面的東西,真正吸引人的,還是瓦格納的音樂。歌劇在瓦格納手里,不再是一系列的詠嘆調。瓦格納是在用音樂講故事。音樂是歌劇的一部分, 歌唱是音樂的一部分,布景、燈光和舞臺效果都是不可分割的一分子。所有這些是一個整體,除了個別出彩的樂段(基本上只有“飛馳的女武神”這一段,曾被用在 電影“現代啟示錄”中),很少單獨演奏。它們戲劇性太強了,脫離了歌劇,就僅僅是一堆音符而已。
                這一點上,C++也是如此,語言、庫、慣用法等等,都是整體,一旦相互脫離,便無法發揮應有的作用。所以孤立地運用C++某一方面的特性,往往會誤入歧途,只有綜合運用各種手段,才能真正地用好C++。
                瓦格納龐大復雜和莫扎特的簡潔優雅形成了鮮明的對比。但是我們不能說莫扎特比瓦格納更好,或者反過來。他們的音樂都是最偉大的杰作,一個人可以毫無沖突地 同時成為他們倆人的粉絲(就像我:))。我們總能從中獲得想像、思考、思想和倫理,兩者都具備無法替代的營養。認真地學習和吸收,才是正道。
                編程語言的學習和使用,又何嘗不是如此呢?

            注:嚴格地說,瓦格納的這些作品并不是歌劇,有一個正式的名稱:music drama,直接翻譯是“音樂戲劇”,是歌劇的擴展。但是為了方便,還是廣義上地將其稱作歌劇。
            posted @ 2008-02-14 11:25 longshanks 閱讀(1203) | 評論 (1)編輯 收藏

            2008年1月25日 #

            當GPL遇上MP

            莫華楓

                GPL,也就是General Purpose Language,是我們使用的最多的一類語言。傳統上,GPL的語法,或者特性,是固態的。然而,程序員都是聰明人(即便算不上“最聰明”,也算得上 “很聰明”吧:)),往往不愿受到語法的束縛,試圖按自己的心意“改造”語言。實際上,即便是早期的語言,也提供了一些工具,供聰明人們玩弄語法。我看的第一本C語言的書里,就有這么一個例子,展示出這種“邪惡”的手段:
                  #define procedure void
                  #define begin {
                  #define end }
                然后:
                  procedure fun(int x)
                  begin
                      ...
                  end
                實際上,它的意思是:
                  void fun(int x)
                  {
                      ...
                  }
                這可以看作是對初學C語言的Pascal程序員的安慰。這種蹩腳的戲法可以算作元編程的一種,在一種語言里創造了另一個語法。不過,實在有些無聊。然而,在實際開發中,我們或多或少地會需要一些超出語法范圍的機制。有時為了完善語言,彌補一些缺憾;有時為了增強一些功能;有時為了獲得一些方便。更新潮的,是試圖在一種GPL里構建Domain Specific Language,或者說“子語言”,以獲得某個特性領域上更直觀、更簡潔的編程方式。這些對語言的擴展需求的實現,依賴于被稱為Meta- Programming(MP)的技術。
                另一方面,隨著語言功能和特性的不斷增加,越來越多的人開始抱怨語言太復雜。一方面:“難道我們會需要那些一輩子也用不到幾回的語言機制,來增加語言的復雜性和學習使用者的負擔嗎?”。另一方面:“有備無患,一個語言機制要到迫在眉睫的時候才去考慮嗎?”。但MP技術則將這對矛盾消弭于無形。一種語言,可以簡潔到只需最基本的一些特性。而其他特定的語言功能需求,可以通過MP加以擴展。如果不需要某種特性,那么只要不加載相應的MP代碼即可,而無需為那些機制而煩惱。
                MP最誘人的地方,莫過于我們可以通過編寫一個代碼庫便使得語言具備以往沒有的特性。
                然而,全面的MP能力往往帶來巨大的副作用,以至于我們無法知道到底是好處更多,還是副作用更多。語言的隨意擴展往往帶來某些危險,比如語法的沖突和不兼容,對基礎語言的干擾,關鍵字的泛濫等等。換句話說,MP是孫悟空,本領高強。但沒有緊箍咒,是管不住他的。
                那么,緊箍咒是什么呢?這就是這里打算探討的主題。本文打算通過觀察兩種已存在的MP技術,分析它們的特點與缺陷,從中找出解決問題的(可能)途徑。

            AST宏

                首先,先來看一下宏,這種遠古時代遺留下來的技術。以及它的后裔,ast宏。
                關于傳統的宏的MP功能,上面的代碼已經簡單地展示了。但是,這種功能是極其有限的。宏是通過文本替換的形式,把語言中的一些符號、操作符、關鍵字等等替換成另一種形式。而對于復雜的語法構造的創建無能為力。問題的另一面,宏帶來了很多副作用。由于宏的基礎是文本替換,所以幾乎不受語法和語義的約束。而且,宏的調試困難,通常也不受命名空間的約束。它帶來的麻煩,往往多于帶來的好處。
                ast宏作為傳統宏的后繼者,做了改進,使得宏可以在ast(Abstract Syntax Tree)結構上執行語法的匹配。(這里需要感謝TopLanguage上的Olerev兄,他用簡潔而又清晰的文字,對我進行了ast宏的初級培訓:))。這樣,便可以創造新的語法:
                  syntax(x, "<->", y, ";")
                  {
                      std::swap(x, y);
                  }
                當遇到代碼:
                  x <-> y;
                的時候,編譯器用std::swap(x,y);加以替換。實際上,這是將一種語法結構映射到另一個語法結構上。而ast宏則是這種映射的執行者。
                但是,ast宏并未消除宏本身的那些缺陷。如果x或者y本身不符合swap的要求(類型相同,并且能復制構造和賦值,或者擁有swap成員函數),那么 ast宏調用的時候無法對此作出檢驗。宏通常以預編譯器處理,ast宏則將其推遲到語法分析之時。但是此時依然無法得到x或y的語義特征,無法直接在調用點給出錯誤信息。
                同時,ast宏還是無法處理二義性的語法構造。如果一個ast宏所定義的語法構造與主語言,或者其他ast宏的相同,則會引發混亂。但是,如果簡單粗暴地將這種“重定義”作為非法處理,那么會大大縮小ast宏(以及MP)的應用范圍。實際上,這種語法構造的重定義有其現實意義,可以看作一種語法構造的“重載”,或者函數(操作符)重載的一種擴展。
                解決的方法并不復雜,就是為ast宏加上約束。實際上,類似的情形在C++98的模板上也存在,而C++則試圖通過為模板添加concept約束加以解決。這種約束有兩個作用:其一,在第一時間對ast宏的使用進行正確性檢驗,而無需等到代碼展開之后;其二,用以區分同一個語法構造的不同版本。
                于是,對于上述例子可以這樣施加約束(這些代碼只能表達一個意思,還無法看作真正意義上的MP語法):
                  syntax(x, "<->", y, ";")
                       where x,y is object of concept (has_swap_mem or (CopyConstructable and Assignable))
                            && typeof(x)==typeof(y)
                  {
                      std::swap(x,y);
                  }
                如此,除非x,y都是對象,并且符合所指定的concept,否則編譯器會當即加以拒絕,而且直截了當。
                不過,如此變化之后,ast宏將不會再是宏了。因為這種約束是語義上的,必須等到語義分析階段,方能檢驗。這就超出了宏的領地了。不過既然ast宏可以從預處理階段推遲到語法分析階段,那么再推遲一下也沒關系。再說,我們關注的是這種功能,帶約束的ast宏到底是不是宏,也無關緊要。

            TMP

                下面,我們回過頭,再來看看另一種MP技術——TMP(參考David Abrahams, Aleksey Gurtovoy所著的《C++ Template Metaprogramming》)。對于TMP存在頗多爭議,支持者認為它提供了更多的功能和靈活性;反對者認為TMP過于tricky,難于運用和調試。不管怎么樣,TMP的出現向我們展示了一種可能性,即在GPL中安全地進行MP編程的可能性。由于TMP所運用的都是C++本身的語言機制,而這些機制都是相容的。所以,TMP所構建的 MP體系不會同GPL和其他子語言的語法機制相沖突。
                實際上,TMP依賴于C++的模板及其特化機制所構建的編譯期計算體系,以及操作符的重載和模板化。下面的代碼摘自boost::spirit的文檔:
                  group = '(' >> expr >> ')';

                  expr1 = integer | group;

                  expr2 = expr1 >> *(('*' >> expr1) | ('/' >> expr1));

               expr = expr2 >> *(('+' >> expr2) | ('-' >> expr2));

                這里表達了一組EBNF(語法著實古怪,這咱待會兒再說):>>代表了標準EBNF的“followed by”,*代表了標準EBNF的*(從右邊移到左邊),括號還是括號,|依舊表示Union。通過對這些操作符的重載,賦予了它們新的語義(即EBNF的相關語義)。然后配合模板的類型推導、特化等等機制,變戲法般地構造出一個語法解析器,而且是編譯時完成的。

                盡管在spirit中,>>、*、|等操作符被挪作他用,但是我們依然可以在這些代碼的前后左右插入諸如:cin>> *ptrX;的代碼,而不會引發任何問題。這是因為>>等操作符是按照不同的類型重載的,對于不同類型的對象的調用,會調用不同版本的操作符重載,互不干擾,老少無欺。

                但是,TMP存在兩個問題。其一,錯誤處理不足。如果我不小心把第二行代碼錯寫成:expr1 = i | group;,而i是一個int類型的變量,那么編譯器往往會給出一些稀奇古怪的錯誤。無非就是說類型不匹配之類的,但是很晦澀。這方面也是TMP受人詬病的一個主要原因。好在C++0x中的concept可以對模板作出約束,并且在調用點直接給出錯誤提示。隨著這些技術的引入,這方面問題將會得到緩解。

                其二,受到C++語法體系的約束,MP無法自由地按我們習慣的形式定義語法構造。前面說過了,spirit的EBNF語法與標準EBNF有不小的差異,這對于spirit的使用造成了不便。同樣,如果試圖運用TMP在C++中構造更高級的DSL應用,諸如一種用于記賬的帳務處理語言,將會遇到更大的限制。實際上TMP下的DSL也很少有令人滿意的。

                所以說,TMP在使用上的安全性來源于操作符復用(或重載)的特性。但是,操作符本身的語法特性是固定的,這使得依賴于操作符(泛化或非泛化)重載的TMP不可能成為真正意義上的MP手段。

                那么,對于TMP而言,我們感興趣的是它的安全性和相容性。而對其不滿的,則是語法構造的靈活性。本著“去其糟粕,取其精華”的宗旨,我們可以對TMP做一番改進,以獲得更完整的MP技術。TMP的核心自然是模板(類模板和函數/操作符模板),在concept的幫助下,模板可以獲得最好的安全性和相容性。以此為基礎,如果我們將模板擴展到語法構造上,那么便可以在保留TMP的安全性和相容性的情況下,獲得更大的語法靈活性。也就是說,我們需要增加一種模板——語法模板

                  template<typename T>
                  syntax synSwap=x "<->" y ";"
                     require SameType<decltype(x), T> && SameType<decltype(y), T>
                            && (has_swap_mem<T> || (CopyConstructable<T> and Assignable<T>)
                  {
                      std::swap(x, y);
                  }

                這里我杜撰了關鍵字syntax,并且允許為語法構造命名,便于使用。而語法構造描述,則是等號后面的部分。require沿用自C++0x的concept提案。稍作了些改造,允許concept之間的||。

            用戶定義的語法

                如果比較帶約束的ast宏和語法模板,會發現極其相似。實際上,兩者殊途同歸,展示了幾乎完全相同的東西。ast宏和TMP分別位于同一個問題的兩端,當它們的缺陷得到彌補時,便會相互靠攏,最終歸結到一種形式上。 

                現在,通過結合兩種方案的特色,我們便可以得到一個最終的MP構造,既安全,又靈活:

                  syntax synSwap=x "<->" y ";";

                  where

                      (x,y is object)

                      && SameType<x, y>

                      && (has_swap_mem<x> || (CopyConstructable<x> and Assignable<x>))

                  {

                      std::swap(x, y);

                  }

                我去掉了template關鍵字,在約束(where)的作用下,template和類型參數列表都已經沒有必要了。同時,也允許直接將對象放入 concept:Assignable<x>,這相當于:Assignable<decltype<x>>。

                “is ...”是特別的約束,用來描述那些超越concept范疇的特性。“...”可以是關鍵字“object”,表明相應的標識符是對象;也可以是關鍵字 “type”,表明相應的標識符是類型;或者是關鍵字“concept”,指定相應的標識符是concept等等。操作符的重載所涉及的參數只會是對象,只需對其類型做出約束,因此concept便足夠使用。但語法構造的定義則廣大的多,它不僅僅會涉及對象,更可能涉及其它類型的語法要素。實際上,語法構造所面對的參數是“標識符”。那么一個標識符上可能具備的各種特性,都應當作為約束的內容。這里大致歸納出以下幾點需要約束的特性:

            1. 分類。對象、類型、concept、函數等等。is ...;
            2. 來源。來自哪個namespace。from ...;
            3. 尺寸。類型大小。sizeof(x)>20;
            4. 從屬。指定一個語法構造是否從屬于其他語法構造(其他語法構造的一部分)。如果是從屬語法構造,那么將不能單獨出現在獨立的語句中。更進一步,可以強行指定一個語法構造從屬于某個語法構造,不允許在其他語法構造中使用。belong_to ...;
            5. 類型及對象間的關系。諸如繼承、子對象、子類型、true typedef、別名、成員等等。
            6. ...

                也可以認為,語法構造的約束是concept的自然延伸。concept對類型做出約束,而語法構造的約束的對象,則是標識符。

                為了強化語法構造的創建能力,應當允許在語法構造的定義中使用BNF,或其他類似的語法描述語言。

                在語法構造約束的支援下,便可以化解一些子語言同宿主語言之間的語法沖突。比如,我們創建了一種類似spirit的EBNF子語言,可以按照標準的EBNF形式編寫語法,構造一個語法解析器:

                  syntax repeat1=p '+'

                  where

                      p is object of type(Parser)

                  {...}

                這樣便定義了一個后綴形式的+操作符,但僅僅作用于類型Parser的對象上。對于如下的代碼:

                  letter + number

                使用主語言的+(加),還是使用EBNF的+(重復,至少一次),取決于letter和number的特征(這里是類型):

                  int letter, number;

                  letter + number; //使用主語言的+,表示letter加number

                  Parser letter, number;

                  letter + number; //使用EBNF的+,表示一串letter后面跟一個number

                如此,便使得用戶定義的語法不會同主語言的相同語法發生沖突。

                但是,語法構造的約束并不能完全消除所有的語法沖突。其中一種情況便是常量的使用。比如:

                  'x' + 'y'

                它在宿主語言中,會被識別為兩個字符的相加。但在BNF子語言中,則會是若干個字符'x'之后緊跟一個‘y'。由于沒有類型、對象等代碼實體的參與,編譯器無法分辨使用哪種語言的語法規則來處理。解決的方法是使得常量類型化。這種做法在C++等語言中已經運用多年。通常采用為常量加前后綴:

                  'x'b_ + 'y'b_

                以此代表BNF子語言的常量。常量的語法定義可以是:

                  syntax bnf_const="'" letter "'" "b_";

                  where

                      is const of ... //...可以是某種類型

                  {...}

                通過is const of約束,將所定義的常量與某個類型綁定。而通過類型,編譯器便可以推斷出應當使用那種語法對其處理。

                然而,盡管帶約束的語法構造定義可以在很大程度上消除語法沖突和歧義,但不可能消除所有的語法矛盾。另一方面,結構相似,但語義完全不同的語法構造混合在一起,即便不引發矛盾,也會對使用者造成難以預計的混亂。因此,在實際環境下,需要通過某種“語法圍欄”嚴格限定某種用戶定義語法的作用范圍。最直接的形式,就是通過namespace實現。namespace在隔離和協調命名沖突中起到很好的作用,可以進一步將其運用到MP中。由于namespace一經打開,其作用范圍不會超出最內層的代碼塊作用域:

                  {

                      using namespace XXX;

                      ...

                  } //XXX的作用范圍不會超出這個范圍

                運用這個特性,可以很自然地將蘊藏在一個namespace中的用戶定義語法構造限制在一個確定的范圍內。

                但是,僅此不夠。畢竟語法不同于命名,不僅會存在沖突,還會存在(合法的)混淆,而且往往是非常危險的。為此,需要允許在using namespace的時候進一步指定語法的排他性。比如:

                  {

                      using namespace XXX exclusive;

                      ...

                  }

                如果namespace中的用戶定義語法與外部語法(宿主語言,或外圍引用的namespace)重疊(沖突或混淆),外部語法將被屏蔽。更進一步,可以允許不同級別的排他:排斥重疊和完全屏蔽。前者只屏蔽重疊的語法,這種級別通常用于擴展性的用戶語法構造(擴展主語言,需要同主語言協同工作。而且語法重疊較少);而后者則將外部語法統統屏蔽,只保留所打開的namespace中的語法(子語言在代碼塊中是唯一語言),這種級別用于相對獨立的功能性子語言(可以獨立工作的子語言,通常會與主語言和其他子語言間存在較大的語法重疊,比如上述BNF子語言等等)。

                為提供更多的靈活性,可以在完全屏蔽的情況下,通過特定語句引入某種不會引發沖突的外部語法。比如:

                  {

                      using namespace XXX exclusive full;

                      using host_lang::syntax(...); //引入主語言的某個語法,...代表語法名稱

                      ...

                  }

                通常情況下,子語言不會完全獨立運作,需要同宿主語言或其他子語言交互,也就是數據交換。主語言代碼可能需要將數據對象傳遞給子語言,處理完成后再取回。由于編譯器依賴于標識符的特性(主要是類型)來選擇語法構造。所以,一種子語言必須使用其內部定義的類型和對象。為了能夠交換數據,必須能夠把主語言的對象包裝成子語言的內部類型。通過在子語言的namespace中進行true typedef,可以將主語言類型定義成一個新的(真)類型,在進入子語言作用域的時候,做顯式的類型轉換。另外,為了方便數據轉換,還可以采用兩種方法:將主語言類型map到一個子語言類型(相當于給予“雙重國籍”),進入子語言的語法范圍時,相應的對象在類型上具有兩重性,可以被自動識別成所需的類型,但必須在確保不發生語法混淆的情況下使用;另一種穩妥些的方法,可以允許執行一種typedef,介于alias和true typedef之間,它定義了一個新類型,但可以隱式地同原來的類型相互轉換。數據交換的問題較復雜,很多問題尚未理清,有待進一步考察。

            總結

                作為MP手段,ast宏擁有靈活性,而TMP則具備安全性,將兩者各自的優點相結合,使我們可以獲得更加靈活和安全的語法構造定義手段。通過在用戶定義的語法構造上施加全面的約束,可以很好地規避語法沖突和歧義。

                但是,需要說明的是,這里所考察的僅僅局限在用戶定義語法構造的沖突和歧義的消除上。GPL/MP要真正達到實用階段,還需要面對更多問題。比如,由于存在用戶定義的語法構造,語法分析階段所面對的語法不是固態的,需要隨時隨地接受新語法,甚至重疊的語法(存在多個候選語法,不同的候選語法又會產生不同的下級語法),這就使語法分析大大復雜;語法模式匹配被推遲到語義分析階段,此前將無法對某些語法錯誤作出檢驗;一個語法構造的語義需要通過宿主語言定義,如何銜接定義代碼和周邊的環境和狀態;如何為用戶定義的語法構造設置出錯信息;由于某些語法構造的二義性,如何判別語法錯誤屬于哪個語法構造;...。此外還有一些更本質性的問題,諸如語法構造的重載和二義性是否會更容易誘使使用者產生更多的錯誤等等,牽涉到錯綜復雜的問題,需要更多的分析和試驗。

                另外,作為MP的重要組成部分,編譯期計算能力也至關重要。TMP運用了C++模板特化,D語言通過更易于理解的static_if等機制,都試圖獲得編譯期的計算能力,這些機制在完整的MP中需要進一步擴展,而并非僅僅局限在與類型相關的計算上。其他一些與此相關的特性,包括反射(編譯期和運行期)、類型traits等,也應作為MP必不可少的特性。

            posted @ 2008-01-25 15:09 longshanks 閱讀(1334) | 評論 (4)編輯 收藏

            2008年1月6日 #


            GP技術的展望——道生一,一生二

            by  莫華楓



                長期以來,我們始終把GP(泛型編程)作為一種輔助技術,用于簡化代碼結構、提高開發效率。從某種程度上來講,這種觀念是對的。因為迄今為止,GP技術還只是一種編譯期技術。只能在編譯期發揮作用,一旦軟件完成編譯,成為可執行代碼,便失去了利用GP的機會。對于現在的多數應用而言,運行時的多態能力顯得尤為重要。而現有的GP無法在這個層面發揮作用,以至于我這個“GP迷”也不得不灰溜溜地聲稱“用OOP構建系統,用GP優化代碼”。

                然而,不久前,在TopLanguage group上的一次討論,促使我們注意到runtime GP這個概念。從中,我們看到了希望——使GP runtime化的希望——使得GP有望在運行時發揮其巨大的威力,進一步為軟件的設計與開發帶來更高的效率和更靈活的結構。
                在這個新的系列文章中,我試圖運用runtime GP實現一些簡單,但典型的案例,來檢測runtime GP的能力和限制,同時也可以進一步探討和展示這種技術的特性。

            運行時多態

                現在的應用側重于交互式的運作形式,要求軟件在用戶輸入下作出響應。為了在這種情況下,軟件的整體結構的優化,大量使用組件技術,使得軟件成為“可組裝” 的系統。而接口-實現分離的結構形式很好地實現了這個目標。多態在此中起到了關鍵性的作用。其中,以OOP為代表的“動多態”(也稱為 “subtyping多態”),構建起在運行時可調控的可組裝系統。GP作為“靜多態”,運用泛化的類型體系,大大簡化這種系統的構建,消除重復勞動。另外還有一種鮮為人知的多態形式,被《C++ Template》的作者David Vandevoorde和Nicolai M. Josuttis稱為runtime unbound多態。而原來的“動多態”,即OOP多態,被細化為runtime bound多態;“靜多態”,也就是模板,則被稱為static unbound多態。
                不過這種稱謂容易引起誤解,主要就是unbound這個詞上。在這里unbound和bound是指在編寫代碼時,一個symbol是否同一個具體的類型 bound。從這點來看,由于GP代碼在編寫之時,面向的是泛型,不是具體的類型,那么GP是unbound的。因為現有的GP是編譯期的技術,所以是 static的。OOP的動多態則必須針對一個具體的類型編寫代碼,所以是bound的。但因為動多態可以在運行時確定真正的類型,所以是runtime 的。至于runtime unbound,以往只出現在動態語言中,比如SmallTalk、Python、Ruby,一種形象地稱謂是“duck-typing”多態。關于多態的更完整的分類和介紹可以看這里
                通過動態語言機制實現的runtime unbound,存在的性能和類型安全問題。但當我們將GP中的concept技術推廣到runtime時會發現,rungime unbound可以擁有同OOP動多態相當的效率和類型安全性,但卻具有更大的靈活性和更豐富的特性。關于這方面,我已經寫過一篇文章 ,大致描述了一種實現runtime concept的途徑(本文的附錄里,我也給出了這種runtime concept實現的改進)。

            Runtime Concept

                Runtime concept的實現并不會很復雜,基本上可以沿用OOP中的“虛表”技術,并且可以更加簡單。真正復雜的部分是如何在語言層面表達出這種runtime GP,而不對已有的static GP和其他語言特性造成干擾。在這里,我首先建立一個基本的方案,然后通過一些案例對其進行檢驗,在發現問題后再做調整。
                考慮到runtime concept本身也是concept,那么沿用static concept的定義形式是不會有問題的:
                  concept myconcept<T> {
                      T& copy(T& lhs, T const& rhs);
                      void T::fun();
                      ...
                  }
                具體的concept定義和使用規則,可以參考C++0x的concept提案這篇文章 ,以及這篇文章
                另一方面,我們可以通過concept_map將符合一個concept的類型綁定到該concept之上:
                  concept_map myconcept<MyType> {}
                相關內容也可參考上述文件。
                有了concept之后,我們便可以用它們約束一個模板:
                  template<myconcept T>void myfun(T const& val); //函數模板
                  template<myconcept T>class X  //類模板
                  {
                       ...
                  };
                到此為止,runtime concept同static concept還是同一個事物。它們真正的分離在于使用。對于static concept應用,我們使用一個具體的類型在實例化(特化)一個模板:
                  X<MyType> x1;  //實例化一個類模板
                  MyType obj1;
                  myfun(obj1);  //編譯器推導obj1對象的類型實例化函數模板
                  myfun<MyType>(obj1);  //函數模板的顯式實例化
                現在,我們將允許一種非常規的做法,以使runtime concept成為可能:允許使用concept實例化一個模板,或定義一個對象
                  X<myconcept> x2;
                  myconcept* obj2=new myconcept<MyType>;
                  myfun(obj2);  //此處,編譯器將會生成runtime版本的myfun
                這里的含義非常明確:對于x2,接受任何符合myconcept的類型的對象。obj2是一個“動態對象”(這里將runtime concept引入的那種不知道真實類型,但符合某個concept的對象稱為“動態對象”。而類型明確已知的對象成為“靜態對象”),符合myconcept要求。至于實際的類型,隨便,只要符合myconcept就行。
                這種情形非常類似于傳統動多態的interface。但是,它們有著根本的差異。interface是一個具體的類型,并且要求類型通過繼承這種形式實現這個接口。而concept則不是一種類型,而是一種“泛型”——具備某種特征的類型的抽象(或集合),不需要在類型創建時立刻與接口綁定。與 concept的綁定(concept_map)可以發生在任何時候。于是,runtime concept實際上成為了一種非侵入的接口。相比interface這種侵入型的接口,更加靈活便捷。
                通過這樣一種做法,我們便可以獲得一種能夠在運行時工作的GP系統。
                在此基礎上,為了便于后續案例展開,進一步引入一些有用的特性:
            1. 一個concept的assosiate type被視為一個concept。一個concept的指針/引用(concept_id*/concept_id&,含義是指向一個符合concept_id的動態對象,其實際類型未知),都被視作concept。一個類模板用concept實例化后,邏輯上也是一個concept。
            2. 動態對象的創建。如果需要在棧上創建動態對象,那么可以使用語法:concept_id<type_id> obj_id; 這里concept_id是concept名,type_id是具體的類型名,obj_id是對象名稱。這樣,便在棧上創建了一個符合concept_id的動態對象,其實際類型是type_id
              如果需要在堆上創建動態對象,那么可以用語法:concept_id* obj_id=new concept_id<type_id>; 這實際上可以看作“concept指針/引用”。
            3. concept推導(編譯期)。對于表達式concept_id obj_id=Exp,其中Exp是一個表達式,如果表達式Exp的類型是具體的類型,那么obj_id代表了一個靜態對象,其類型為Exp的類型。如果表達式Exp的類型是concept,那么obj_id是一個動態對象,其類型為Exp所代表的concept。
              那么如何確定Exp是具體類型還是concept?可以使用這么一個規則:如果Exp中涉及的對象,比如函數的實參、表達式的操作數等等,只要有一個是動態對象(類型是concept),那么Exp的類型就是concept;反之,如果所有涉及的對象都是靜態對象(類型為具體的類型),那么Exp的類型為相應的具體類型。同樣的規則適用于concept*或concept&。
            4. concept轉換。類似在類的繼承結構上執行轉換。refined concept可以隱式地轉換成base concept,反過來必須顯式地進行,并且通過concept_cast操作符執行。兄弟concept之間也必須通過concept_cast轉換。
            5. 基于concept的重載,也可以在runtime時執行,實現泛化的dynamic-dispatch操作。

                下面,就開始第一個案例。

            案例:升級的坦克

                假設我們做一個游戲,主題是開坦克打仗。按游戲的慣例,消滅敵人可以得到積分,積分到一定數量,便可以升級。為了簡便起見,我們只考慮對主炮升級。第一級的主炮是90mm的;第二級的主炮升級到120mm。主炮分兩種,一種只能發射穿甲彈,另一種只能發射高爆彈。因此,坦克也分為兩種:能打穿甲彈的和能打高爆彈的。
                為了使代碼容易開發和維護,我們考慮采用模塊化的方式:開發一個坦克的框架,然后通過更換不同的主炮,實現不同種類的坦克和升級:
                  //一些基本的concept定義
                  //炮彈頭concept
                  concept Warheads<typename T> {
                      double explode(TargetType tt); //炮彈爆炸,返回殺傷率。不同彈頭,對不同類型目標殺傷率不一樣。
                  }
                  //炮彈concept,我們關心的當然是彈頭,所以用Warheads定義一個associate type
                  concept Rounds<typename T> {
                      Warheads WH;
                      ...
                  }
                  //主炮concept
                  concept Cannons<typename T> {
                      Rounds R;
                      void T::load(R& r); //裝填炮彈,load之后炮彈會存放在炮膛里,不能再load,除非把炮彈打出去
                      R::WH T::fire();   //開炮,返回彈頭。發射后炮膛變空,可以再load
                  }
                  //類型和模板定義
                  //坦克類模板
                  template<Cannons C>
                  class Tank
                  {
                      ...
                  public:
                      void load(typenam C::R& r) {
                          m_cannon.load(r);
                      }
                      typename C::R::WH fire() {
                          return m_cannon.fire();
                      }
                  private:
                      C m_cannon;
                  };
                  //主炮類模板
                  template<Rounds Rd>
                  class Cannon
                  {
                  public:
                      typedef Rd R;
                      void load(R& r) {...}
                      typename R::WH fire() {...}
                  }
                  template<Rounds Rd> concept_map Cannons<Cannon<Rd>>{}
                  //炮彈類模板
                  template<Warheads W>
                  class Round
                  {
                  public:
                      typedef W WH;
                      static const int caliber=W::caliber;
                      W shoot() {...}
                      ...
                  };
                  template<Warhead W> concept_map<Round<W>>{}
                  //彈頭類模板,通過traits把各類彈頭的不同行為彈頭的代碼框架分離,使類型“可組裝”
                  concept WH_Traits<T> {
                      return T::exploed(int, TargetType, double, Material);
                  }
                  template<WH_Traits wht, int c>
                  class Warhead
                  {
                  public:
                      const static int caliber=c;
                      double explode(TargetType tt) {
                          return wht::exploed(c, tt, ...);
                      }
                      ...
                  };
                  template<WH_Traits WHT, int c> concept_map<Warhead<WHT, c>>{}
                  //彈頭traits
                  struct KE_WHTraits
                  {
                      static double exploed(int caliber, TargetType tt, double weight, Material m) {...}
                  };
                  concept_map<KE_WHTraits>{}
                  struct HE_WHTraits
                  {
                      static double exploed(int caliber, TargetType tt, double weight, Material m) {...}
                  };
                  concept_map<HE_WHTraits>{}
                  //定義各類彈頭
                  typedef Warhead<KE_WHTraits, 90> WH_KE_90;
                  typedef Warhead<KE_WHTraits, 120> WH_KE_120;
                  typedef Warhead<HE_WHTraits, 90> WH_HE_90;
                  typedef Warhead<HE_WHTraits, 120> WH_HE_120;
                  //定義各類炮彈
                  typedef Round<WH_KE_90> Round_KE_90;
                  typedef Round<WH_KE_120> Round_KE_120;
                  typedef Round<WH_HE_90> Round_HE_90;
                  typedef Round<WH_HE_120> Round_HE_120;
                  //定義各類主炮
                  typedef Cannon<Round_KE_90> Cannon_KE_90;
                  typedef Cannon<Round_KE_120> Cannon_KE_120;
                  typedef Cannon<Round_HE_90> Cannon_HE_90;
                  typedef Cannon<Round_HE_120> Cannon_HE_120;
                  //定義各類坦克
                  typedef Tank<Cannon_KE_90> Tank_KE_90;
                  typedef Tank<Cannon_KE_120> Tank_KE_120;
                  typedef Tank<Cannon_HE_90> Tank_HE_90;
                  typedef Tank<Cannon_HE_120> Tank_HE_120;
                于是,當我們開始游戲時,就可以按照玩家的級別創建坦克對象,并且射擊:
                  //第一級玩家,駕駛發射90mm高爆炮彈的坦克
                  Tank_HE_90 myTank;
                  Round_HE_90 r1;
                  myTank.load(r1);
                  myTank.fire();
                  //第二級玩家,駕駛發射120mm穿甲彈的坦克
                  Tank_KE_120 yourTank;
                  Round_KE_120 r2;
                  yourTank.load(r2);
                  yourTank.fire();
                  //如果這樣,危險,炮彈不匹配,小心炸膛
                  myTank.load(r2); //error
                到目前為止,這些代碼僅僅展示了靜態的GP。concept在這里也只是起到了類型參數約束的作用。但是,在這些代碼中,我們可以明顯地看到,在運用GP 的參數化類型特性之后,可以很容易地進行組件化。對于一組具備類似行為和結構特征的類型,我們可以通過模板的類型參數,將差異部分抽取出來,獨立成所謂的 “traits”或者“policy”。并且通過traits或policy的組合構成不同的產品。在某些復雜的情況下,traits和policy還可以進一步通過traits或policy實現組件化。
                接下來,很自然地應當展開runtime GP的運用了。
                一個游戲者是可以升級的,為了使得這種升級變得更加靈活,我們會很自然地使用Composite模式。現在,我們可以在Runtime concept的支援下實現GP版的Composite模式:
                  //坦克的concept
                  concept tanks<T> {
                      typename Round;
                      void T::load(Round&);
                      Round::WH T::fire();
                  }
                  concept_map tanks<Tank_KE_90>{}
                  concept_map tanks<Tank_HE_90>{}
                  concept_map tanks<Tank_KE_120>{}
                  concept_map tanks<Tank_HE_120>{}
                  //坦克構造函數模板
                  template<tanks T>
                  T* CreateTank(WHType type, int level) { //WHType是一個枚舉表明炮彈種類
                      switch(level)
                      {
                      case 1:
                          if(type==wht_KE)
                              return new tanks<Tank_KE_90>;
                          else
                              return new tanks<Tank_HE_90>;
                      case 2:
                          if(type==wht_KE)
                              return new tanks<Tank_KE_120>;
                          else
                              return new tanks<Tank_HE_120>;
                      default:
                          throw error("no such tank.");
                      }
                  }
                  //玩家類
                  class player
                  {
                  public:
                      void update() {
            m_pTank=CreateTank(m_tankType, ++m_level);
                      }
                      ...
                  private:
                      int m_level;
                      WHType m_tankType;
            tanks* m_pTank;
                  };
                在類player中,使用了一個concept,而不是一個類型,來定義一個對象。根據前面提到的concept推導規則,m_pTank指向一個動態對象,還是靜態對象,取決于為它賦值的表達式類型是concept還是具體類型。在update()函數中,可以看到,m_pTank通過表達式CreateTank(m_tankType, ++m_level)賦值。那么這個函數的返回類型,將決定m_pTank的類型。CreateTank()是一個模板,返回類型是模板參數,并且是符合concept tanks的類型。關鍵在于代碼中的return new tanks<...>語句。前文已經說過,這種形式是使用<...>中的類型創建一個符合tanks的動態對象。所以,CreateTank()返回的是動態對象。那么,m_pTank也將指向一個動態對象。在運行時,當玩家達到一定條件,便可以升級。update()成員函數將根據玩家的級別重新創建相應的坦克對象,賦值到m_pTank中。
                這里,實際上是利用tanks這個concept描述,充當類型的公有接口。它所具有的特性同動多態的抽象基類是非常相似的。但是所不同的是,如同我在代碼中展現的那樣,concept作為接口,可以在任何時候定義,同類型綁定。而無需象抽象基類那樣,必須在類型定義之前定義。于是,這種非侵入式的接口相比抽象基類擁有更加靈活自由的特性。
                然而,事情還沒有完。在進一步深化坦克案例后,我們還將發現runtime GP擁有更加有趣和重要的特性。
                坦克開炮為的是攻擊目標。對目標的毀傷情況直接關系到玩家的生存和得分。所以,我們必須對射擊后,目標的損毀情況進行計算。于是編寫了這樣一組函數:
                  double LethalityEvaluate(Target& t, double hitRate, WH_KE_90& wh) {...}
                  double LethalityEvaluate(Target& t, double hitRate, WH_HE_90& wh) {...}
                  double LethalityEvaluate(Target& t, double hitRate, WH_KE_120& wh) {...}
                  double LethalityEvaluate(Target& t, double hitRate, WH_HE_120& wh) {...}
                Target是目標;hitRate是命中率,根據坦克和目標的位置、射擊參數綜合計算獲得(如果想要更真實,可以加上風向、風力、氣溫、濕度、海拔等等因素);wh就是發射出來的炮彈了。函數返回殺傷率。如此,我們便可以在射擊之后進行評估了:
                  double l=LethalityEvaluate(house, hr, myTank.fire());
                 現在,游戲需要進行一些擴展,增加一個升級,允許坦克升級到第三級。到了第三極,主炮的口徑就升到頭了,但可以升級功能,可以發射穿甲彈和高爆彈。這樣,我們就需要一個“兩用”的主炮類。但是,實際上并不需要直接做這么一個類,只需要用“兩用”的彈藥來實例化Cannon模板:
                  concept Warheads120<T> : Warheads<T> { //120mm炮彈頭concept
                      double LethalityEvaluate(Target& t, double hitRate, T& wh);
                  }
                  concept Rounds120<T> : Rounds<T> {}
                  concept_map Warheads120<WH_KE_120> {}  //120mm的穿甲彈屬于Warheads120
                  concept_map Warheads120<WH_HE_120> {}  //120mm的高爆彈屬于Warheads120
                  template<WH120 WH> concept_map Rounds120<Round<WH>> {} //所有彈頭是Warheads120的炮彈都是屬于Rounds120
                  typedef Canon<Rounds120> Cannon120; //用Rounds120實例化Cannon模板,得到“兩用”主炮
                一堆炫目的concept和concept_map之后,得到Rounds120,就是所謂的“兩用”彈藥。作為一個concept,它同兩種類型map 在一起,實際上就成了這兩個類型的接口。當我們使用Rounds120實例化Cannon<>模板時,也就創建了一個“兩用的主炮”(使用 Rounds120彈藥的主炮)。如果用這個Cannon120實例化Tank模板,那么就可以得到第三級坦克(裝上Cannon120主炮的坦克就是第三級):
                  typedef Tank<Cannon120> TankL3;
                于是,我們可以使用不同的120mm彈藥裝填主炮,并且發射相應的炮彈:
                  TankL3 tank_l3;
                  Round_KE_120 ke_round;  //創建一枚穿甲彈
                  Round_HE_120 he_round;  //創建一枚高爆彈
                  tank_l3.load(ke_round);  //裝填穿甲彈
                  tank_l3.fire();               //發射穿甲彈
                  tank_l3.load(he_round);  //裝填高爆彈
                  tank_l3.fire();               //發射高爆彈
                現在,我們把注意力從消滅敵人,轉移到TankL3::load()的參數類型和TankL3::fire()的返回類型上。在一級和二級坦克(類型 Tank_KE_90等)上,load()成員的參數類型是Round_KE_90等具體的類型;而fire()的返回類型亦是如此。但TankL3是用 Cannon120實例化的,而Cannon120是用Rounds120這個concept實例化的。根據Tank<>模板的定義, load()成員的參數類型實際上是模板參數上的一個associate type。而這個associate type實際上就是Rounds120。這意味著load()實例化后的簽名是:void load(Rounds120& r)(這里暫且允許concept作為類型參數使用)。只要符合Rounds120的類型都可以作為實參傳遞給load()成員。同樣,fire()成員的返回類型來自于Round120上的associate type,也是個concept。因此,fire()實例化后的簽名是:Warheads120 fire()。
                接下來值得注意的是fire()成員。它返回類型是一個concept,那么返回的將是一個動態對象。在運行時,它可能返回WH_KE_120的實例,也可能返回WH_HE_120的實例,取決于運行時load()函數所裝填的炮彈類型。當我們采用LethalityEvaluate()函數對該炮彈的殺傷情況進行評估將會出現比較微妙的情況:
                  double x=LethalityEvaluate(hisTank, hr, tank_l3.fire());
                這時候,編譯器應當選擇哪個LethalityEvaluate()?由于tank_l3.fire()返回的是一個動態對象,具體的類型編譯時不知道。實際上,在正宗的靜態語言中,這樣的調用根本無法通過編譯。當然,編譯器可以通過runtime reflect獲得類型信息,然后在LethalityEvaluate()的重載中匹配正確的函數。然而,這種動態語言做法會造成性能上的問題,為靜態語言所不屑。
                但是,在這里,在runtime concept的作用下,我們可以使這種調用成為靜態的、合法的,并且是高效的。請注意我在concept Warhead120的定義中加入了一個associate function:double LethalityEvaluate(Target& t, double hitRate, T& wh);。runtime concept會很忠實地將concept定義中的associate function構造到一個函數指針表(我稱之為ctable)中。(詳細情況請看本文附錄和這篇文章的附錄)。因此,與tank_l3.fire()返回的動態對象實際類型對應的LethalityEvaluate()函數版本的指針正老老實實地躺在相應的ctable里。所以,我們可以直接從動態對象上獲得指向ctable的指針,并且找出相應的LethalityEvaluate()函數指針,然后直接調用即可。比如:
                  tank_l3.load(ke_round);
                  double x=LethalityEvaluate(hisTank, hr, tank_l3.fire());
                在這些代碼的背后,ke_round通過load()裝填入主炮后,便搖身變成了一個動態對象。編譯器會為它附加上指向ctable的指針,然后在調用 fire()的時候返回指向這個動態對象的引用。此時,編譯器發現這個動態對象所對應的Warhead120 concept上已經定義了一個名為LethalityEvaluate()的associate function,并且簽名與當前調用相符。于是,便可以直接找到ctable中LethalityEvaluate()對應的那個函數指針,無所顧忌的調用。由于一個concept的associate function肯定是同實際類型匹配的函數版本。比如,對于WH_HE_120而言,它的associate function LethalityEvaluate()是版本:double LethalityEvaluate(Target& t, double hitRate, WH_HE_120& wh) {...}。其他版本的LethalityEvaluate()都無法滿足concept Warhead120施加在類型WH_HE_120上的約束。
                這個特性就使得runtime concept作為接口,相比動多態的抽象接口,具有更大的靈活性。抽象接口只表達了類的成員,以及類本身的行為,無法表達類型同其他類型的互動關系,或者說類型間的交互。而concept同時描述了成員函數和相關的自由函數(包括操作符),使得類型間的關系也可以通過接口直接獲得,無需再通過 reflect等間接的動態手段。
                這一點在處理內置類型(如int、float)、預置類型(某些庫中的類型)、第三方類型等不易或無法修改的類型有至關重要的作用。在OOP下,我們無法輸出一個“整數”,或許是short、或許是long,甚至是unsinged longlong。為此,我們要么把它們轉換成一個“最基礎類型”(C/C++的void*,或C#的Object*),然后運用rtti信息進行類型轉換,再做處理;要么使用variant這種類型包裝(就像COM中的那樣),然后為他們全面定義一套計算庫。但runtime concept不僅僅允許輸出“整數”這樣一個動態對象,而且還將相關的各種操作附在動態對象之上,使之無需借助rtti或者輔助類型也可進行各類處理,就如同處理具體類型的對象那樣。
                但是,在這里我僅僅考察了針對一個類型的concept(暫且稱之為一元concept),還未涉及兩個和兩個以上類型的concept(暫且稱為多元 concept,或n-元concept)。在實際開發中,多數操作都會涉及多個對象,比如兩個數相加、一種類型轉換成另一種。此時,我們將會面對多元的 concept。但是多元的runtime concept的特性還不清楚,還需要進一步研究分析。

            總結

                本文初步展示了在引入runtime concept之后,GP的動態化特性。歸納起來有以下幾點:
            1. static GP和runtime GP之間在形式上完全統一,兩者可以看作同一種抽象機制的不同表現。因此,我們在構造類型、函數等代碼實體的時候,并不需要考慮它們將來需要作為static使用,還是runtime使用。static和runtime的控制完全取決于這些代碼實體的使用方式。這就很好地減少了軟件項目早期設計,以及庫設計的前瞻性方面壓力。
            2. runtime concept作為非侵入式的接口,可以非常靈活地使用。我們無需在代碼編寫的一開始就精確地定義好接口,可以先直接編寫功能類型,逐步構建軟件結構。需要時再定義接口(concept),并可以在任何時候與類型綁定。接口的定制可以成為一個逐步推進的過程,早期的接口設計不足產生的不良影響相應地弱化了。
            3. runtime concept相比動多態的抽象接口更加自由。concept可以對類型的成員函數、自由函數、類型特征等等方面的特性作出描述。在runtime化之后,相關自由函數成為了接口的一部分。更進一步規約了類型在整體軟件的代碼環境中的行為特征。同時,也為動態對象的訪問提供更多的信息和手段。
            4. concept不僅實現類型描述,還可以進一步描述類型之間的關系。這大大完善了抽象體系。特別在runtime情況下,這種更寬泛的類型描述能力可以起到兩個作用:其一,進一步約束了動態對象的行為;其二,為外界操作和使用類型提供更多的信息,消除或減少了類型匹配方面的抽象懲罰。這個方面的更多特性尚不清楚,還需要更進一步地深入研究。
                綜上所述,我們可以看到runtime GP在不損失性能的情況下,具備相比動多態更靈活、更豐富的手段。從根本上而言,以concept為核心的GP提供了更基礎的抽象體系(關于這方面探討,請看我的這篇文章中關于concept對類型劃分的作用部分)。或者說,concept的類型描述和約束作用體現了類型抽象的本質,而在此基礎上進一步衍生出static和runtime兩種具體的使用方式。這也就是所謂:道生一,一生二。:)

            附錄

            Runtime Concept實現方案二

                我在這篇文章附錄里,給出了一種實現runtime concept的可能方案。這里,我進一步對這個方案做了一些改進,使其更加精簡、高效。
                假設我們有一個concept:
                concept Shape<T>
                {
                    void T::load(xml);
                    void T::draw(device);
                    void move(T&);
                }
                另外,還有一個代表圓的concept:
                concept Cycles<T> :
                    CopyConstructable<T>,
                    Assignable<T>,
                    Swappable<T>,
                    Shape<T>
                {
                    T::T(double, double, double);
                    double T::getX();
                    double T::getY();
                    double T::getR();
                    void T::setX(double);
                    void T::setY(double);
                    void T::setR(double);
                }
                現在有類型Cycle:
                class Cycle
                {
                public:
                    Cycle(double x, double y, double r);
                    Cycle(Cycle const& c);
                    Cycle& operator=(Cycle const& c);
                    void swap(Cycle const& c);
                    void load(xml init);
                    void draw(device dev);
                    double getX();
                    double getY();
                    double getR();
                    void setX(double x);
                    void setY(double y);
                    void setR(double r);
                private:
                    ...
                };
                我們將類型Cycle map到concept Cycles上:
                  concept_map Cycles<Cycle>{}
                當我們創建對象時,將會得到如下圖的結構:

            runtime concept-2

                concept表(concept list)不再同對象放在一起,而是同一個類型的類型信息放在一起。一同放置的還有ctable。ctable中每個對應的concept項都有一個指向 concept表的指針(也可以指向類型信息頭),用以找到concept list,執行concept cast。動態對象,或動態對象的引用/指針上只需附加一個指向相應的concept的指針即可。相比前一個方案,內存利用率更高。

            posted @ 2008-01-06 17:17 longshanks 閱讀(3058) | 評論 (7)編輯 收藏

            2007年12月17日 #

            被誤解的C++——漢尼拔

            by 莫華楓

                公元前216年8月2日,意大利東部平原,一個叫做坎尼的地方,兩支大軍擺開陣勢,準備決一死戰。一方是由保羅斯和瓦羅兩位執政官率領的羅馬人,另一方則是偉大的軍事天才漢尼拔*巴卡率領的迦太基軍隊及其同盟。羅馬人超過8萬,而迦太基僅有4萬余人。然而到了傍晚,羅馬人被徹底擊敗,7萬人被殺,僅有少數得以逃脫。這就是著名的坎尼會戰。經此一役,(外加先前進行的特利比亞和特拉西梅諾湖會戰),羅馬人元氣大傷,成年公民損失達五分之一。部分城邦背叛羅馬,西西里也發生起義。羅馬已經到了搖搖欲墜的地步。

                漢尼拔的這些勝利,完全得益于先前的一次異乎尋常的遠征。公元前218年,漢尼拔率領軍隊,從新迦太基城(西班牙)出發,翻越比利牛斯山,進入南高盧地域。在他面前有兩條路可走,翻越阿爾俾斯山,或者沿海岸進入意大利。但是,當時羅馬人已在沿海地區部署了兩支部隊,準備攔截漢尼拔。而且,羅馬人的海軍優勢,使得他們可以在任何時候將一支部隊登陸在他的背后。而翻越阿爾俾斯山,則是一條及其艱險的道路,更何況是在冬天。

                漢尼拔選擇了阿爾俾斯山。他甩開了羅馬人,從小圣貝納德和日內瓦山之間越過阿爾俾斯山,進入意大利境內。此時,羅馬人便失去了戰略縱深,一把尖刀已經深深地插入他們的腹內...

             

                C++的發展史上,也有著如同漢尼拔翻越阿爾俾斯山遠征。一切還得從C with Class時代說起。

                Bjarne曾經反復強調,他創建C++為的是將Simular的抽象能力同C的性能結合起來。于是,在C語言的基礎上,誕生了一種擁有類、繼承、重載等等面向對象機制的語言。在這個階段,C++提供了兩個方面的抽象能力。一種是數據抽象,也就是將數據所要表達的含義通過類型以及依附于類型上的成員表述。另一種則是多態,一種最原始的多態(重載)。

                數據抽象,通過被稱為“抽象數據類型(ADT)”的技術實現。ADT的一種方案,就是類。類所提供的封裝性將一個數據實體的外在特征,或者說語義的表述形式,同具體的實現,比如數據存儲形式,分離。這樣所增加的中間層將數據的使用者同數據的實現者隔離,使得他們使用共同的約定語義工作,不再相互了解彼此的細節,從而使得兩者得以解耦。

                多態則是更加基礎更加重要的一種特性。多態使得我們得以用同一種符號實現某種確定的語義。多態的精髓在于:以一種形式表達一種語義。在此之前,我們往往被迫使用不同的符號來代表同一種抽象語義,為的是適應強類型系統所施加的約束。比如:

            //代碼#1
            int add_int(int lhs, int rhs);
            float add_float(float lhs, float rhs);

                很顯然,這兩個函數表達的語義分別是“把兩個int類型值加在一起”和“把兩個float類型值加在一起”。這兩個語義抽象起來都表達了一個意思:加。

                我們在做算術題的時候是不會管被計算的數字是整數還是實數。同樣,如果能夠在編程的時候,不考慮算術操作對象的類型,只需關心誰和誰進行什么操作,那么會方便得多。當C++引入重載后,這種愿望便得以實現:

            //代碼#2
            int add(int lhs, int rhs);
            float add(float lhs, float rhs);

                重載使得我們只需關心“加”這個語義,至于什么類型和什么類型相加,則由編譯器根據操作數的類型自動解析。

                從某種意義上說,重載是被長期忽視,但卻極為重要的一個語言特性。在多數介紹OOP的書籍中,重載往往被作為OOP的附屬品,放在一些不起眼的地方。它的多態本質也被動多態的人造光環所設遮蔽。然而,重載的重要作用卻在實踐中潛移默化地體現出來。重載差不多可以看作語言邁入現代抽象體系的第一步。它的實際效用甚至要超過被廣為關注的OOP,而不會像OOP那樣在獲得抽象的同時,伴隨著不小的副作用。

                 隨著虛函數的引入,C++開始具備了頗具爭議的動多態技術。虛函數是一種依附于類(OOP的類型基礎)的多態技術。其技術基礎是后期綁定(late-binding)。當一個類D繼承自類B時,它有兩種方法覆蓋(override)B上的某個函數:

            //代碼#3
            class B
            {
            public:
                void fun1();
                virtual void fun2();
            };

            class D:public B
            {
            public:
                void fun1();
                void fun2();
            };

                當繼承類D中聲明了同基類B中成員函數相同函數名、相同簽名的成員函數,那么基類的成員函數將被覆蓋。對于基類的非虛成員函數,繼承類會直接將其遮蔽。對于類型D的使用者,fun1代表了D中所賦予的語義。而類型D的實例,可以隱式地轉換成類型B的引用b,此時調用b的fun1,則執行的是類B的fun1 定義,而非類D的fun1,盡管此時b實際指向一個D的實例。

                但是,如果繼承類覆蓋了基類的虛函數,那么將得到相反的結果:當調用引用b的fun2,實際上卻是調用了D的fun2定義。這表明,覆蓋一個虛函數,將會在繼承類和基類之間的所有層次上執行覆蓋。這種徹底的、全方位的覆蓋行為,使得我們可以在繼承類上修飾或擴展基類的功能或行為。這便是OOP擴展機制的基礎。而這種技術被稱為動多態,意思是基類引用所表達的語義并非取決于基類本身,而是來源于它所指向的實際對象,因此它是“多態”的。因為一個引用所指向的對象可以在運行時變換,所以它是“動”的。

                隨著動多態而來的一個“副產品”,卻事實上成為了OOP的核心和支柱。虛函數的“動多態”特性將我們引向一個極端的情況:一個都是虛函數的類。更重要的,這個類上的虛函數都沒有實現,每個虛函數都未曾指向一個實實在在的函數體。當然,這樣的類是無法直接使用的。有趣的是,這種被稱為“抽象基類”的類,迫使我們繼承它,并“替它”實現那些沒有實現的虛函數。這樣,對于一個抽象基類的引用,多態地擁有了繼承類的行為。而反過來,抽象基類實際上起到了強迫繼承類實現某些特定功能的作用。因此,抽象基類扮演了接口的角色。接口具有兩重作用:一、約束繼承類(實現者)迫使其實現預定的成員函數(功能和行為);二、描述了繼承類必定擁有的成員函數(功能和行為)。這兩種作用促使接口成為了OOP設計體系的支柱。

                C++在這方面的進步,使其成為一個真正意義上具備現代抽象能力的語言。然而,這種進步并非“翻越阿爾俾斯山”。充其量也只能算作“翻越比利牛斯山”。對于C++而言,真正艱苦的遠征才剛開始,那令人生畏的“阿爾俾斯山”仍在遙遠的前方。

                同漢尼拔一樣,當C++一腳邁入“現代抽象語言俱樂部”后,便面臨兩種選擇。或者在原有基礎上修修補補,成為一種OOP語言;或者繼續前進,翻越那座險峻的山峰。C++的漢尼拔——Bjarne Stroustrup——選擇了后者。

                從D&E的描述中我們可以看到,在C++的原始設計中就已經考慮“類型參數”的問題。但直到90年代初,才真正意義上地實現了模板。然而,模板只是第一步。諸如Ada等語言中都有類似的機制(泛型,generic),但并未對當時的編程技術產生根本性的影響。

                關鍵性的成果來源于Alex Stepanov的貢獻。Stepanov在后來被稱為stl的算法-容器庫上所做的努力,使得一種新興的編程技術——泛型編程(Generic Programming,GP)——進入了人們的視野。stl的產生對C++的模板機制產生了極其重要的影響,促使了模板特化的誕生。模板特化表面上是模板的輔助特性,但是實際上它卻是比“類型參數”更加本質的機能。

                假設我們有一組函數執行比較兩個對象大小的操作:

            //代碼#4
            int compare(int lhs, int rhs);
            int compare(float lhs, float rhs);
            int compare(string lhs, string rhs);

                重載使得我們可以僅用compare一個函數名執行不同類型的比較操作。但是這些函數具有一樣的實現代碼。模板的引入,使得我們可以消除這種重復代碼:

            //代碼#5
            template<typename T> int compare(T lhs, T rhs) {
                if(lhs==rhs)
                    return 0;
                if(lhs>rhs)
                    return 1;
                if(lhs<rhs)
                    return -1;
            }

                這樣一個模板可以應用于任何類型,不但用一個符號表達了一個語義,而且用一個實現代替了諸多重復代碼。這便是GP的基本作用。

                接下來的變化,可以算作真正意義上的“登山”了。

                如果有兩個指針,分別指向兩個相同類型的對象。此時如果我們采用上述compare函數模板,那么將無法得到所需的結果。因為此時比較的是兩個指針的值,而不是所指向的對象本身。為了應付這種特殊情況,我們需要對compare做“特別處理”:

            //代碼#6
            template<typename T> int compare(T* lhs, T* rhs) {
                if(*lhs==*rhs)
                    return 0;
                if(*lhs>*rhs)
                    return 1;
                if(*lhs<*rhs)
                    return -1;
            }

                這個“特殊版本”的compare,對于任何類型的指針作出響應。如果調用時的實參是一個指針,那么這個“指針版”的compare將會得到優先匹配。如果我們將compare改成下面的實現,那么就會出現非常有趣的行為:

            //代碼#7
            template<typename T>
            struct comp_impl
            {
                int operator()(T lhs, T rhs) {
                    if(lhs==rhs)
                        return 0;
                    if(lhs>rhs)
                        return 1;
                    if(lhs<rhs)
                        return -1;
                }
            };
            template<typename T>
            struct comp_impl<T*>
            {
                int operator()(T* lhs, T* rhs) {
                    comp_impl<T>()(*lhs, *rhs);
                }
            };
            template<typename T> int compare(T* lhs, T* rhs) {
                comp_impl<T>()(*lhs, *rhs);
            }

                當我們將指針的指針作為實參,調用compare時,神奇的事情發生了:

            //代碼#8
            double **x, **y;
            compare(x, y);

                compare居然成功地剝離了兩個指針,并且正確地比較了兩個對象的值。這個戲法充分利用了類模板的局部特化和特化解析規則。根據規則,越是特化的模板,越是優先匹配。T*版的comp_impl比T版的更加“特化”,會得到優先匹配。那么當一個指針的指針實例化comp_impl,則會匹配T*版的 comp_impl,因為指針的指針,也是指針。T*版通過局部特化機制,剝離掉一級指針,然后用所得的類型實例化comp_impl。指針的指針剝離掉一級指針,那么還是一個指針,又會匹配T*版。T*版又會剝離掉一級指針,剩下的就是真正可以比較的類型——double。此時,double已無法與 T*版本匹配,只能匹配基礎模板,執行真正的比較操作。

                這種奇妙的手法是蘊含在模板特化中一些更加本質的機制的結果。這種意外獲得的“模板衍生產品”可以算作一種編譯時計算的能力,后來被一些“好事者”發展成獨立的“模板元編程”(Template Meta Programming,TMP)。

                盡管TMP新奇而又奧妙,但終究只是一種輔助技術,用來彌補C++的一些缺陷、做一些擴展,“撿個漏”什么的。不過它為我們帶來了兩點重要的啟示:一、我們有可能通過語言本身的一些機制,進行元編程;二、元編程在一定程度上可以同通用語言一起使用。這些啟示對編程語言的發展有很好的指導意義。

                模板及特化規則是C++ GP的核心所在。這些語言特性的強大能力并非憑空而來。實際上有一只“幕后大手”在冥冥之中操縱著一切。

                假設有一個類型系統,包含n個類型:t1,...,tn,那么這些類型構成了一個集合T={t1,...,tn}。在當我們運用重載技術時,實際上構造了一組類型的tuple到函數實現的映射:<ti1,ti2,ti3,...> ->fj()。編譯器在重載解析的時候,就是按照這組映射尋找匹配的函數版本。當我們編寫了形如代碼#5的模板,那么就相當于構建了映射:<T, T,...> ->f0()。

                而代碼#6,以及代碼#7中的T*版模板,實際上是構造了一個<Tp>->fp()的映射。這里Tp是T的一個子集:Tp={t'|t'=ti*, ti∈T}。換句話說,特化使泛型體系細化了。利用模板特化技術,我們已經能夠(笨拙地)分辨浮點數、整數、內置類型、內置數組、類、枚舉等等類型。具備為類型劃分的能力,也就是構造不同的類型子集的能力。

                現在,我們便可以構造一個“泛型體系”:G={T} U T U Tp U Ta U Ti U Tf U Tc ...。其中,Tp是所有指針類型,Ta是數組,Ti是整數,Tf是浮點數,Tc是類等等。但是如果我們按照泛化程度,把G中的元素排列開:{T, Tp, Ta, Ti,...,t1,...,tn}。我們會發現這中間存在一些“斷層”。這些斷層位于T和Tp等之間,以及Tp等與ti等之間等等。這表明在C++98/03中,抽象體系不夠完整,存在缺陷。

                所以,到目前為止,C++還沒有真正翻越阿爾俾斯山里那座最險峻的山峰。這正是C++0x正在努力做的,而且勝利在望。

                在C++0x中,大牛們引入了first-class的concept支持。concept目前還沒有正式的法定描述(以及合理的中文翻譯)。通俗地講,concept描述了一個類型的(接口)特征。說具體的,一個concept描述了類型必須具備的公共成員函數,必須具備的施加在該類型上的自由函數(和操作符),以及必須具備的其他特征(通過間接手段)。下面是一個典型的concept:

            concept has_equal<T>
            {
                bool T::equal(T const& v);
            };

                這個concept告訴我們它所描述的類型必須有一個equal成員,以另一個同類型的對象為參數。當我們將這個concept施加在一個函數模板上,并作為對類型參數的約束,那么就表明了這個模板對類型參數的要求:

            template<has_equal T>bool is_equal(T& lhs, T const& rhs) {
                return lhs.equal(rhs);
            }

                如果實參對象的類型沒有equal成員,那么is_equal將會拒絕編譯通過:這不是我要的!

                concept是可以組合的,正式的術語叫做“refine”。我們可以通過refine進一步構造出約束更強的concept:

            concept my_concept<T> : has_equal<T>, DefaultConstructable<T>, Swappable<T> {}

                refine獲得的concept將會“繼承”那些“基concept”的所有約束。作為更細致的組合手段,concept還可以通過!操作符“去掉”某些內涵的concept約束:

            concept my_concept1<T> : has_equal<T>, !DefaultConstructable<T> {}

                這個concept要求類型具備equal成員,但不能有默認構造函數。

                通過這些手段,concept可以“無限細分”類型集合T。理論上,我們可以制造一連串只相差一個函數或者只相差一個參數的concept。

                一個concept實際上就是構成類型集合T的劃分的約束:Tx={ti| Cx(ti)==true, ti∈T}。其中Cx就是concept所構造的約束。不同的concept有著不同范圍的約束。這樣,理論上我們可以運用concept枚舉出類型集合 T的所有子集。而這些子集則正好填補了上述G中的那些斷層。換句話說,concept細化了類型劃分的粒度,或者說泛型的粒度。使得“離散”的泛型系統變成“連續”的。

                當我們運用concept約束一個函數模板的類型參數時,相當于用concept所描述的類型子集構建一個映射:<Tx1,Tx2,...>->fx()。凡是符合tuple <Tx1,Tx2,...>的類型組合,對應fx()。所以,從這個角度而言,函數模板的特化(包括concept)可以看作函數重載的一種擴展。在concept的促進下,我們便可以把函數模板特化和函數重載統一在一個體系下處理,使用共同的規則解析。

                在目前階段,C++差不多已經登上了“抽象阿爾俾斯山”的頂峰。但是就如同漢尼拔進入意大利后,還需要面對強盛的羅馬共和國,與之作戰那樣。C++的面前還需要進一步將優勢化作勝利。要做的事還很多,其中最重要的,當屬構建Runtime GP。目前C++的GP是編譯時機制。對于運行時決斷的任務,還需要求助于OOP的動多態。但是C++領域的大牛們已經著手在Runtime GP和Runtime Concept等方面展開努力。這方面的最新成果可以看這里這里這里。相信經過若干年的努力后,GP將會完全的成熟,成為真正主流的編程技術。

             

                坎尼會戰之后,漢尼拔已經擁有了絕對的優勢。羅馬人已經戰敗,他同羅馬城之間已經沒有任何強大的敵對力量,羅馬人也已經聞風喪膽,幾無斗志。但是,漢尼拔卻犯下了或許令他一生后悔的錯誤。他放過了羅馬城,轉而攻擊羅馬的南部城邦和同盟。他低估了羅馬人的意志,以及羅馬同盟的牢固程度。羅馬人很快結束了最初的混亂,任命了新的執政官,采用了堅壁清野、以柔克剛的新戰略。隨著時間的推移,漢尼拔和他的軍隊限于孤立無援的境地,被迫為了生存而作戰。盡管迫降并占領了幾個羅馬城市,但是終究無法再次獲得給予羅馬人致命一擊的機會。

                漢尼拔的戰略錯誤實際上在從新迦太基城出發之前已經注定。因為漢尼拔對羅馬人的遠征的根本目的并非擊潰并占領羅馬,而是通過打擊羅馬,削弱他們的勢力,瓦解他們的聯盟。以達到尋求簽訂和平協議的目的。然而這種有限戰略卻使導致了他的最終失敗。

             

                不幸的是,C++或多或少地有著同漢尼拔一樣的戰略錯誤。C++最初的目的基本上僅僅局限于“更好的C”。并且全面兼容C。這在當時似乎很合理,因為C可以算作最成功的“底層高級語言”,擁有很高的性能和靈活性。但是,C的設計并未考慮將來會有一個叫做“C++”的語言來對其進行擴展。結果很多優點對于C而言是優點,但卻成了C++的負擔。比如,C大量使用操作符表達語法結構,對于C而言顯得非常簡潔,但對于C++,則使其被迫大規模復用操作符,為其后出現的很多語法缺陷埋下了伏筆。這一點上,Ada做得相對成熟些。它從Pascal那里繼承了主要語法,但不考慮兼容。這使得Ada更加完整,易于發展。新語言就是新語言,過分的兼容是鐐銬,不是優勢。而且,合理地繼承語法,同樣可以吸引眾多開發者。從經驗來看,程序員對于語法變化的承受能力還是很強的。他們更多地關心語言的功能和易用性。

                另一方面,C++最初并未把目標定在“創建一種高度抽象,又確保性能的語言”。縱觀C++的發展,各種抽象機制并非在完整的規劃或路線圖的指導下加入語言。所有高級特性都是以“添油戰術”零打碎敲地加入語言。從某種程度上來看,C++更像是一種實驗性語言,而非工業語言。C++的強大功能和優點是長期積累獲得的,而它的諸多缺陷也是長期添加特性的結果。

                漢尼拔和C++給予我們一個很好的教訓。對于一個試圖在1、20年后依然健康成長的語言,那么就必須在最初便擁有明確的目標和技術發展規劃。對于以往的語言特性應當擇優而取,不能照單全收。并且在技術上擁有足夠的前瞻性。我們知道,技術前瞻性是很難做到的,畢竟技術發展太快。如果做不到,那就得有足夠的魄力對過去的東西加以取舍。所謂“舍小就大,棄子爭先”。

                總體而言,C++在抽象機制的發展方面,還算是成功的。盡管伴隨著不少技術缺陷,但是C++的抽象能力在各種語言中可稱得上出類拔萃。而且C++還在發展,它未來將會發展成什么形態,不得而知。但是,無論C++是繼續修修補補,還是根本性地變革,它的抽象能力都會不折不扣地保留,并且不斷完善和增強。

             

                坎尼會戰之后,漢尼拔又打過幾次小規模的勝仗。但經過長期的作戰,也得不到迦太基的支援,漢尼拔的力量越來越弱,只能在意大利半島上勉強生存。羅馬很快恢復了元氣,改革了軍事體系和作戰方式,重新掌握了戰略主動權。更重要的是,羅馬也有了自己的“漢尼拔”——(征服非洲的)普布利烏斯·科爾內利烏斯·西庇阿(大西庇阿)。西庇阿被派往北非大陸,直接攻擊迦太基人的老巢。漢尼拔被召回,在扎馬與西庇阿擺開陣勢,展開一場決戰。最終,西庇阿運用從漢尼拔那里學到的戰術擊潰了迦太基人,為羅馬人贏得了第二次布匿戰爭的勝利。

                此后,漢尼拔在羅馬人的通緝之下,流亡于地中海沿岸,試圖尋求東山再起的機會。但最終未能如愿,被迫于公元前183年自盡,享年64歲。有趣的是,他的老對手,小他12歲的西庇阿也于同年去世。一個偉大的傳奇就此結束。

            posted @ 2007-12-17 11:28 longshanks 閱讀(2303) | 評論 (11)編輯 收藏

            2007年12月6日 #

                本文來源于TopLanguage Group 上的一次討論(這里這里這里 )。pongba提出:C++的抽象機制并不完善,原因是為了性能而做的折中,未來隨著計算能力的提高到一定程度,人們就能夠忽略更好的抽象所帶來的負面效應。就此諸老大各自提出高見,受益良多啊。經過討論,我基本上理解了pongba的想法。但我覺得等待計算機的性能提高太消極了。我相信隨著編程技術的發展,這種最優抽象造成的性能損失將會越來越小。這種途徑將會更快地讓人們接受最優抽象形式。

                 在“C++ Template”一書中,將多態總結為三種主要類型:runtime bound、static unbound和runtime unbound。其中runtime bound就是我們通常所說的動多態,OOP的核心支柱(廣義上OOP還包括Object Base(OB,僅指類型封裝等OO的基本特性),但有時也會將OB和OOP分開,OOP單指以OO為基礎的動多態。這里使用狹義的OOP含義); static unbound就是靜多態,通過模板實現。而runtime unbound則是一種不常見的形式。早年的SmallTalk具有這種形式,現在的ruby也引入這種機制。
                 在主流的(靜態)語言中,我們會面臨兩種類型的多態需求:對于編譯期可以確定類型的,使用靜多態,比如實例化一個容器;對于運行期方能確定類型的,則使用 動多態。而runtime unbound也可以用于運行期類型決斷。于是,便有了兩種運行期多態。這兩種多態的特性和他們的差異,是本文的核心。實際上,相比動多態, runtime unbound多態為我們提供了更本質的運行時多態手段,我們可以從中獲得更大的收益。但是鑒于一些技術上的困難,runtime unbound多態無法進入主流世界。不過,由于新的編程技術的出現,使得這種更好的運行時多態形式可以同動多態一比高下。

            動多態   

                廢話少說,讓我們從一個老掉牙的案例開始吧:編寫一個繪圖程序,圖形包括矩形、橢圓、三角形、多邊形等等。圖形從腳本(比如xml)中讀出,創建后保存在一個容器中備查。通過遍歷容器執行圖形繪制。
                就這么個題目,很簡單,也很熟悉,解釋OOP的動多態最常用的案例。下面我們就從動多態實現開始。
                首先定義一個抽象基類,也就是接口:

                class IShape

                {

                    virtual void load(xml init)=0;

                    virtual void draw(monitor m)=0;

                    ...

                };

                然后定義各種圖形類,并從這個接口上繼承:

                class Rectangle: public IShape

                {

                    void load(xml init) {...}

                    void draw(monitor m) {...}

                    ...

                };

                class Ellipse: public IShape

                {

                    void load(xml init) {...}

                    void draw(monitor m) {...}

                    ...

                };

                ...

             

                void DrawShapes(monitor m, vector<IShape*> const& g)

                {

                    vector<IShape*>::const_iterator b(g.begin()), e(g.end());

                    for(; b!=e; ++b)

                    {

                        (*b)->draw(m);

                    }

                }

                ...

                現在可以使用這些圖形類了:

                vector<IShape*> vg;

                vg.push_back(new Rectangle);

                vg.push_back(new Ellipse);

                ...

                DrawShapes(crt, vg);

                通過接口IShape,我們可以把不同的圖形類統一到一種類型下。但是,通過虛函數的override,由圖形類實現IShape上的虛函數。這可以算老 生常談了。動多態的核心就是利用override和late bound的組合,使得一個基類可以在類型歸一化的情況下,擁有繼承類的語義。OOP設計模式大量運用這種技術,實現很多需要靈活擴展的系統。

            Runtime Unbound

                Runtime Unbound多態混合了靜多態和動多態的特征,即既有類型泛化,又是運行時決斷的。一個最典型的例子就是ruby的函數:
                class x
                   def fun(car)
                        car.aboard
                    end
                end
                這個案例非常明確地展示出了Runtime Unbound多態的特點。car參數沒有類型,這里也不需要關心類型,只要求car對象有一個aboard方法即可。由于ruby是動態語言,能夠運行時檢測對象的特征,并動態調用對象上的方法。
                在Runtime Unbound的思想指導下,我們利用一種偽造的“動態C++”,把上面的繪圖例子重新編寫:

                class Rectangle

                {

                    void load(xml init) {...}

                    void draw(monitor dev) {...}

                    ...

                };

                class Ellipse

                {

                    void load(xml init) {...}

                    void draw(monitor dev) {...}

                    ...

                };

                ...

                void DrawShapes(monitor dev, vector<anything> const& g)

                {

                    vector<IShape>::const_iterator b(g.begin()), e(g.end());

                    for(; b!=e; ++b)

                    {

                        (*b).draw(dev);

                    }

                }

                ...

                vector<anything> vg;

                vg.push_back(Rectangle(...));

                vg.push_back(Ellipse(...));

                ...

                DrawShapes(crt, vg);

                圖形類不再從抽象接口IShape繼承,而用關鍵字anything實例化vector<>模板。這個虛構的anything關鍵字所起的作 用就是使得vector能夠接受不同類型的對象。當DrawShapes()函數接收到存放圖形對象的容器后,遍歷每一個對象,并且調用對象上的draw ()函數,而不管其類型。
                從這段代碼中,我們可以看出Runtime Unbound多態帶來的好處。所有圖形類不再需要歸一化成一個類型(抽象接口)。每個類只需按照約定,實現load、draw等成員函數即可。也就是 說,這些圖形類解耦合了。一旦類型解耦,便賦予我們很大的自由度。最典型的情況就是,我們需要使用一個其他人開發的圖形類,并且無法修改其實現。此時,如 果使用動多態,就很麻煩。因為盡管這些圖形類都擁有load、draw等函數,但畢竟不是繼承自IShape,無法直接插入容器。必須編寫一個繼承自 IShape的適配器,作為外來圖形類的包裝,轉發對其的訪問。表面上,我們只是減少一個接口的定義,但Runtime Unbound多態帶來的解耦有著非凡的意義。因為類耦合始終是OOP設計中的一個令人頭痛的問題。在后面,我們還將看到建立在Runtime Unbound多態基礎上的更大的進步。
                然而,盡管Runtime Unbound多態具有這些優點,但因為建立在動態語言之上,其自身存在的一些缺陷使得這項技術無法廣泛使用,并進入主流。
                Runtime Unbound多態面臨的第一個問題就是類型安全。確切的講是靜態類型安全。
                本質上,Runtime Unbound多態(動態語言)并非沒有類型安全。當動態語言試圖訪問一個未知類型對象的成員時,會通過一些特殊機制或特殊接口獲得類型信息,并在其中尋 找所需的對象成員。如果沒有找到,便會拋出異常。但是,傳統上,我們希望語言能夠在編譯期得到類型安全保證,而不要在運行時才發現問題。也就是說, Runtime Unbound多態只能提供運行時類型安全,而無法得到靜態類型安全。
                第二個問題是性能。Runtime Unbound需要在運行時搜尋類型的接口,并執行調用。執行這類尋找和調用的方法有兩種:反射和動態鏈接。
                反射機制可以向程序提供類型的信息。通過這些信息,Runtime Unbound可以了解是否存在所需的接口函數。反射通常也提供了接口函數調用的服務,允許將參數打包,并通過函數名調用。這種機制性能很差,基本上無法用于稍許密集些的操作。
                動態鏈接則是在訪問對象前在對象的成員函數表上查詢并獲得相應函數的地址,填充到調用方的調用表中,調用方通過調用表執行間接調用。這種機制相對快一些,但由于需要查詢成員函數表,復雜度基本上都在O(n)左右,無法與動多態的O(1)調用相比。
                這些問題的解決,依賴于一種新興的技術,即concept。concept不僅很消除了類型安全的問題,更主要的是它大幅縮小了兩種Runtime多態的性能差距,有望使Runtime Unbound成為主流的技術。

            concept

                隨著C++0x逐漸浮出水面,concept作為此次標準更新的核心部分,已經在C++社群中引起關注。隨著時間的推移,concept的潛在作用也在不斷被發掘出來。
                concept主要用來描述一個類型的接口和特征。通俗地講,concept描述了一組具備了共同接口的類型。在引入concept后,C++可以對模板參數進行約束:
                concept assignable<T> {
                    T& operator=(T const&);
                }
                template<assignable T> void copy(T& a, T const& b) {
                    a=b;
                }
                這表示類型T必須有operator=的重載。如果一個類型X沒有對operator=進行重載,那么當調用copy時,便會引發編譯錯誤。這使得類型參數可以在函數使用之前便能得到檢驗,而無需等到對象被使用時。
                另一方面,concept參與到特化中后,使得操作分派更加方便:
                concept assignable<T> {
                    T& operator=(T const&);
                }
                concept copyable<T> {
                    T& T::copy(T const&);
                }
                template<assignable T> void copy(T& a, T const& b) {    //#1
                    a=b;
                }
                template<copyable T> void copy(T& a, T const& b) {    //#2
                    a.copy(b);
                }
                X x1,x2; //X支持operator=操作符
                Y y1,y2; //Y擁有copy成員函數
                copy(x1, x2);    //使用#1
                copy(y1, y2);    //使用#2
                在靜多態中,concept很好地提供了類型約束。既然同樣是Unbound,那么concept是否同樣可以被用于Runtime Unbound?應當說可以,但不是現有的concept。在Runtime Unbound多態中,需要運行時的concept。
                依舊使用繪圖案例做一個演示。假設這里使用的"C++"已經支持concept,并且也支持了運行時的concept:

                class Rectangle

                {

                    void load(xml init) {...}

                    void draw(monitor dev) {...}

                    ...

                };

                class Ellipse

                {

                    void load(xml init) {...}

                    void draw(monitor dev) {...}

                    ...

                };

                ...

                concept Shape<T> {

                    void T::load(xml init);

                    void T::draw(monitor dev);

                }

                ...

                void DrawShapes(monitor dev, vector<Shape> const& g)

                {

                    vector<IShape>::const_iterator b(g.begin()), e(g.end());

                    for(; b!=e; ++b)

                    {

                        (*b).draw(dev);

                    }

                }

                ...

                vector<Shape> vg;

                vg.push_back(Rectangle(...));

                vg.push_back(Ellipse(...));

                vg.push_back(string("xxx"));    //錯誤,不符合Shape concept

                ...

                DrawShapes(crt, vg);

                乍看起來沒什么特別的,但是請注意vector<Shape>。這里使用一個concept,而不是一個具體的類型,實例化一個模板。這里的意思是說,這個容器接受的是所有符合Shape concept的對象,類型不同也沒關系。當push進vg的對象不符合Shape,便會發生編譯錯誤。

                 但是,最關鍵的東西不在這里。注意到DrawShapes函數了嗎?由于vector<Shape>中的元素類型可能完全不同。語句 (*b).draw(dev);的語義在靜態語言中是非法的,因為我們根本無法在編譯時具體確定(*b)的類型,從而鏈接正確的draw成員。而在這里, 由于我們引入了Runtime Unbound,對于對象的訪問鏈接發生在運行時。因此,我們便可以把不同類型的對象存放在一個容器中。

                concept在這里起到了類型檢驗的作用,不符合相應concept的對象是無法放入這個容器的,從而在此后對對象的使用的時候,也不會發生類型失配的 問題。這也就在動態的機制下確保了類型安全。動多態確保類型安全依靠靜態類型。也就是所有類型都從一個抽象接口上繼承,從而將類型歸一化,以獲得建立在靜 態類型系統之上的類型安全。而concept的類型安全保證來源于對類型特征的描述,是一種非侵入的接口約束,靈活性大大高于類型歸一化的動多態。

                如果我們引入這樣一個規則:如果用類型創建實例(對象),那么所創建的對象是靜態鏈接的,也就是編譯時鏈接;而用concept創建一個對象,那么所創建的對象是動態鏈接的,也就是運行時鏈接。

                在這條規則的作用下,下面這段簡單的代碼將會產生非常奇妙的效果:

                class nShape

                {

                public:

                    nShape(Shape g, int n) : m_graph(g), m_n(n) {}

                    void setShape(Shape g) {

                        m_graph=g;

                    }

                private:

                    Shape    m_graph;

                    int        m_n;

                };

                在規則的作用下,m_graph是一個動態對象,它的類型只有在運行時才能明確。但是無論什么類型,必須滿足Shape concept。而m_n的類型是確定的,所以是一個靜態對象

                這和傳統的模板有區別嗎?模板也可以用不同的類型參數定義成員數據。請看如下代碼:

                Rectangle r;

                Ellipse e;
                 nShape(r, 10);

                 nShape.setShape(e);   //對于傳統模板而言,這個操作是非法的,因為e和r不是同一種類型

                動態對象的特點在于,我們可以在對象創建后,用一個不同類型的動態對象代替原來的,只需要這些對象符合相應的concept。這在靜態的模板上是做不到的。

                現在回過頭來看一下用concept實例化模板的情形。我們知道,用一個類型實例化一個模板,得到的是一個類,或者說類型。而用一個concept實例化 一個模板,得到的又是什么呢?還是一個concept。那么vector<Shape>是一個concept,因而它的實例是動態的對象。當 然,實際上沒有必要把vector<Shape>的實例整個地當成動態對象,它只是具有動態對象的行為特征。在實現上,vector< Shape>可以按照普通模板展開,而其內部由concept模板實參定義的對象作為動態對象處理即可。一個由concept實例化的模板的對象作為語義上的動態對象
                下面的代碼則引出了另一個重要的特性:
                vector<float> vFloat;    //靜態對象的容器,內部存放的都是靜態對象,屬于同一類型float
                vector<Shape> vShape; //動態對象的容器,內部存放動態對象,都符合Shape
                同一個類模板,當使用類型實例化,執行static unbound多態使用concept實例化,執行runtime unbound多態。兩者的形式相同。也就是說static多態同runtime多態以相同的形式表達。 由于concept的加入,兩種完全不同的多態被統一在同一個模型和形式下。實際上,static和runtime unbound多態可以看作同一個抽象體系的兩個分支,分別處理不同情況的應用。而形式上的統一,則更加接近抽象體系的本質。同時,也使得兩種 unbound多態的差異被后臺化,使用者無需額外的工作,便可以同時獲得動態和靜態的抽象能力。同時,兩種多態所展示的邏輯上的對稱性,也暗示了兩者在 本質上的聯系。這里統一的形式,便是這種對稱性的結果。
                對于模板函數,則會表現出更加有趣的特性(這個函數模板有些特別,不需要template關鍵字和類型參數列表,這是我偽造的。但由于concept的使用,它本質上還是一個模板):
                void draw(Shape g);
                這個函數接受一個符合Shape的參數。如果我們用一個靜態對象調用這個函數:
                Rectangle r;
                draw(r);
                那么,就執行static unbound,實例化成一個完完整整的函數,同傳統的函數模板一樣。
                如果用一個動態對象調用這個函數:
                Shape g=Cycle();
                draw(g);
                g=Rectangle();
                draw(g);
                那么,就執行runtime unbound,生成一個等待運行時鏈接的函數。上面的兩次調用,分別進行了兩次運行時鏈接,以匹配不同的動態對象。
                這樣,我們可以通過函數調用時的參數對象,來控制使用不同的多態形式。更復雜的情況就是用一個函數的返回值調用另一個函數,這樣構成的調用鏈依然符合上述的調用控制原則。
                下面,我們將看到Runtime Unbound多態的一個精彩表演:
                //假設,我們已經定義了Rectangle、Cycle、Square、Ellipse、Trangle五個類,
                // 分別map到Rectangles、Cycles、Squares、Ellipses、Trangles五個concept上,
                // 這些concept都refine(可以不正確地理解為繼承吧)自Shape。
                void draw(monitor dev, Rectangles r); //#3
                void draw(monitor dev, Cycles c);       //#4
                void draw(monitor dev, Squares s);    //#5
                void draw(monitor dev, Ellipses e);    //#6
                void draw(monitor dev, Trangles t);    //#7
                //此處定義一個Shape的動態對象
                Shape g=CreateShapeByUserInput();    //這個函數根據用戶輸入創建圖形對象,所以圖形對象的類型只能到運行時從能確定。
                draw(crt, g);
                好了,現在該調用哪個版本的draw?根據用戶的輸入來。換句話說,調用哪個版本的draw,取決于CreateShapeByUserInput()函數的返回結果,也就是用戶輸入的結果。如果CreateShapeByUserInput() 返回Rectangle的動態對象,那么執行#3;如果返回的是Trangle對象,那么執行#7。這是一種動態分派的操作。在運行時concept的作 用下,實現起來非常容易。對draw的調用最終會被轉換成一個concept需求表,來自draw函數,每一項對應一個函數版本,并且指明了所對應的 concept。動態對象上也有一個concept表,每一項存放了這個對象所符合的concept。用這兩個表相互匹配,可以找到g對象的 concept最匹配的那個draw版本,然后調用。
                這實際上是將重載決斷放到運行時進行,而concept在其中起到了匹配參數的作用。
                這樣的做法同利用rtti信息執行類型分派調用類似:
                void draw_impl(monitor dev, Rectangle& r);
                void draw_impl(monitor dev, Cycle& c);
                void draw_impl(monitor dev, Square& s);
                void draw_impl(monitor dev, Ellipse& e);
                void draw_impl(monitor dev, Trangle& t);
                void draw_impl(monitor dev, Shape& g) {
                    if(typeif(g)==typeid(Rectangle))
                        draw_impl(dev, (Rectangle&)g);
                    else if(typeif(g)==typeid(Cycle))
                        draw_impl(dev, (Cycle&)g);
                    ...
                }
                但是,他們卻有著天壤之別。首先,rtti分派是侵入的。如果需要增加一個圖形,需要在draw函數中增加分派代碼。而Runtime Unbound方案則只需要用新的concept重載draw函數即可。
                其次,rtti版本有多少圖形類,就需要多少if...else...,而Runtime Unbound則是一對多的。如果有幾個圖形類內容不同,但有相同的接口,符合同一個concept,那么只需針對concept編寫一個函數版本即可。 比如,如果有一個特別的CycleEx類,使用外界正方形的左上角/右下角坐標描述,正好符合Ellipses concept,那么只需將CycleEx map到Ellipses上即可,無需多加任何代碼。
                最后,rtti需要獲取類型信息,然后做線性比較,性能無法優化。但Runtime Unbound通過concept表的相互匹配,僅牽涉數值操作,有很大的優化空間。
                那么這樣一種運行時分派有什么好處呢?我們看到圖形類上的draw函數接受一個monitor類型參數,它代表設備。如果哪一天需要向另一種設備,比如 printer,輸出圖形,那么就需要在圖形類上增加另一個版本的draw函數。如果類是別人開發的,那么就增加溝通的負擔。如果類是外來的,我們無法修 改,那么只能通過adapter之類的笨拙手段處理。為了讓monitor之類同圖形本身沒有關聯的東西分離,應當使用自由函數執行draw操作。但普通 函數只能接受確定的類型重載,而傳統的函數模板則限于編譯期使用,無法進行運行時分派。所以,如果能夠使用concept重載函數,并且賦予 Runtime Unbound機能,那么便可以用最簡單的形式針對一類類型進行處理,效能高得多。

            運行時concept

               語言層面的concept無法做到這些,因為它是編譯期機制。為此,我們需要有一種運行時的concept,或者說二進制級別的concept。

                一個concept包含了與一個或若干個類型有關的一組函數,包括成員函數和自由函數。于是,我們就可以用一個類似“虛表”的函數指針表(暫且稱為 ctable吧)存放concept指定的函數指針。這樣的ctable依附在動態對象上,就像vtable一樣。每個對象都會匹配和map到若干個 concept。因此,每個動態對象會有一個concept表,其中存放著指向各ctable的指針,以及相應的concept基本信息。

                當一個“用戶”(函數或模板)需要在運行時鏈接到對象上的時候,它會提交一個concept的代碼(全局唯一)。系統用這個代碼在動態對象的 concept表上檢索,獲得指向所需concept的指針,并且填寫到“用戶”給出的一個“插入點”(一個指針)中。隨后“用戶”便可以直接通過這個 “插入點”間接調用所需的函數,成員或自由函數。

                在這里,concept的巧妙之處在于,將一族函數集合在一起,作為一個整體(即接口)。那么,在執行運行時匹配的時候,不再是一個函數一個函數地查詢, 可以一次性地獲知這些函數是否存在。這就很容易地規避了類型安全保證操作的損耗。如果使用hash查詢,那么可以在O(1)實現concept匹配。另 外,一個concept的hash值可以在編譯時計算好,運行時鏈接只需執行hash表檢索,連hash值計算也可以省去。

                一個動態對象可以直接用指向concept ctable的指針表示。在不同concept之間轉換,相當于改變指針的指向,這種操作非常類似OOP中的dynamic_cast。

                對于如下的動態對象定義:

                Shape g=Cycle();

                會創建一個Cycle對象,在對象上構建起一個concept表,表中對應Cycle所有符合的concept。并且建立一組ctable,每個 ctable對應一個concept。每個concept表項指向相應的ctable。而符號g則實際上是指向所建立對象的Shapes ctable的指針。

                對于函數:

                void draw(Shape g);

                draw(g);

                調用g時,由于draw的參數是Shape concept,而g正是draw所需的concept,所以無需在對象g的concept表上匹配,可以直接使用這個ctable指針。這就是說,只要 所用動態對象(g)的concept同使用方(draw函數)能夠匹配,便可以直接使用指向ctable的指針鏈接(編譯時鏈接),無需在運行時重新匹 配。只有發生concept轉換時,才需要在concept表中搜索,獲得所需的ctable指針:

                Swappable s=g; //Swappable是另一個concept

                這種情況同dynamic_cast極其相似。也可以模仿著采用concept_cast之類的操作符,使得concept轉換顯式化,消除隱式轉換的問題(強concept化)。

                所以,Runtime Unbound在運行時concept的作用下,具有同動多態相同的底層行為。因而,他們的性能也是一樣的。很多用于動多態的方案和算法都可以直接用于運行時concept。

            Runtime Unbound和Runtime Bound

                對于runtime unbound同runtime bound之間的差異前面已經有所展示。在其他方面,兩者還存在更多的差別。
                首先,就像繪圖案例中展示的那樣,runtime unbound是非侵入的。runtime unbound不要求類型繼承自同一類型,只需將類型同concept關聯起來便可。
                其次,concept不是一種局限于OO的技術,不僅涉及成員函數,還包括了自由函數,范圍更廣,更加靈活。
                最后,實現上,Runtime Unbound和Runtime Bound之間有驚人的相似之處。兩者都采用一個函數指針表作為操作分派;都采用一個指向函數表的指針作為入口;一個動態對象上的concept之間的轉 換,也同動多態對象一樣,在不同的函數表間切換。他們唯一的不同,是實現接口的機制。
                動多態用類型兼任接口,通過繼承和虛函數實現接口的功能。用類型作為類型的接口,使得這兩個本來獨立的概念交織在一起。增加整個類型體系的復雜度和耦合度。    concept則利用獨立的系統描述、表達和管理接口。類型則回歸到表達業務對象的功能上來。
                動多態在使用類型表達接口的時候,便很容易地引入一個麻煩的問題,表達功能的類型和表達接口的類型混合在一起,使用時必須通過一些方法區分出哪些是接口, 哪些是功能類型。這增加了對象模型的復雜性。而concept則獨立于類型體系之外,所有對接口的操作都是單一的,檢索和匹配來得更加方便快捷。
                作為繼承體系的基礎部分,動多態的抽象接口必須在繼承結構的最頂端。那么這些抽象類型必須先于其他類型出現。這對系統的早期設計產生很大的壓力,往往一個基礎抽象接口設計有誤,便會造成整個體系的變更。
                而concept是獨立于類型的,那么任何時候都可以將一個類型同接口綁定。接口甚至可以在類型體系基本建立之后才確定。這種靈活性對復雜軟件的開發至關重要,去掉了長期以來套在人們頭上的枷鎖。
                前面已經提到,在不需要concept轉換的情況下,無需執行運行時的concept匹配,所有的調用具有同動多態一樣的效率(都是間接調用)。在執行 concept轉換時,無需象動多態那樣在復雜的繼承體系上檢索,只需執行concept表的hash匹配,效率反而更高,而且更簡單。考慮到這些情況, 我們可以認為concept化的Runtime Unbound多態完全能夠替代傳統的動多態。也就是說,我們不再需要動多態了
                 想象一下,如果一門語言能夠擁有運行時concept,那么它完全可以只保留Static Unbound和Runtime Unbound多態,而放棄Runtime Bound多態。一旦放棄動多態(沒有了虛函數和虛表),那么對象模型便可以大大簡化。所有對象只需要線性分布,基類和成員依次堆疊在一起,也沒有 vtable的干擾,對象結構可以做到最簡單。同時,繼承也回歸了代碼重用的傳統用途。而且,對象獨立于接口存儲,在能夠在編譯時靜態鏈接的時候,可以作 為靜態對象使用。而在需要動態對象的地方,又可以很容易地轉換成動態對象,只需要為其附上concept表和ctable。一切都簡化了。對象模型也更加 容易統一。
                這對于很多底層開發的程序員對于c++復雜而又混亂的對象模型難以接受。如果能夠廢除虛函數,簡化對象模型,那么對于這些底層開發而言,將會帶來直接的好 處。只要確保不使用concpt定義對象、實例化模板,便可以使整個軟件執行Static Unbound。這相當于去掉OOP的C++。否則,就啟用Runtime Unbound,實現運行時多態。

            總結

                Static Unbound和Runtime Unbound作為一對親密無間的多態技術,體現了最完善的抽象形式。兩者各踞一方,相互補充,相互支援。而且兩者具有統一的表現形式,大大方便了使用, 對于軟件工程具有非凡的意義。另一方面,Runtime Bound多態作為OO時代的產物,體現了靜態類型語言在運行時多態方面的最大努力。但是,隨著運行時concept的引入,Runtime Unbound多態自身存在的靜態類型安全問題和性能問題,都能夠得到很好的解決。至此,Runtime Unbound便具備了替代Runtime Bound的實力。相信在不久的將來,Runtime Bound將會逐漸步入它的黃昏。

            參考

            1. http://groups.google.com/group/pongba/web/Runtime+Polymorphic+Generic +Programming.pdf。大牛人Jaakko Järvi等寫的關于Runtime concept的文章,講解了runtime concept的概念的實現方法,并在ConceptC++上以庫的形式實現。其中使用傳統的動多態實現runtime concept,這表明動多態的實現機制同runtime concept是一致的。當然庫的實現很復雜,這是“螺螄殼里做道場”,無奈之舉。Runtime concept還是應當在語言中first-class地實現。
            2. http://www.lubomir.org/academic/MinimizingCodeBloat.pdf。也是Jaakko Järvi寫的,運行時分派的文章。
            3. http://opensource.adobe.com/wiki/index.php/Runtime_Concepts。
            4. Inside C++ Object Model。

            附錄 Runtime Concept的具體實現

                我們有一個concept:
                concept Shape<T>
                {
                    void T::load(xml);
                    void T::draw(device);
                    void move(T&);
                }
                另外,還有一個代表圓的concept:
                concept Cycles<T> :
                    CopyConstructable<T>,
                    Assignable<T>,
                    Swappable<T>,
                    Shape<T>
                {
                    T::T(double, double, double);
                    double T::getX();
                    double T::getY();
                    double T::getR();
                    void T::setX(double);
                    void T::setY(double);
                    void T::setR(double);
                }
                現在有類型Cycle:
                class Cycle
                {
                public:
                    Cycle(double x, double y, double r);
                    Cycle(Cycle const& c);
                    Cycle& operator=(Cycle const& c);
                    void swap(Cycle const& c);
                    void load(xml init);
                    void draw(device dev);
                    double getX();
                    double getY();
                    double getR();
                    void setX(double x);
                    void setY(double y);
                    void setR(double r);
                private:
                    ...
                };
                當定義一個動態對象:
                Shape g=Cycle();
                便會形成如下圖的結構:

                g實際上是一個指針,指向concept表的Shape項,而Shape項指向Shape對應的ctable。由于Cycle refine自Shape等眾多concept,那么Cycle的ctable實際上包含了這些concept的ctable,所以只需一個Cycle的 ctable,而其他concept都分別指向其中各自相應的部分。ctable中的每一個項則指向具體的函數體。
                如果遇到語句:
                Swappable h=concept_cast<Swappable>(g);
                那么,將會執行一個搜索,用concept Swappable的id(比如hash碼)在concept表中檢索是否存在Swappable項。如果存在,就將對應項的指針賦給h。這種操作同 dynamic_cast操作非常相似,只是相比在復雜的對象結構中查詢更加簡單迅速。
                concept表置于對象的頭部或尾部,這是為了便于對象檢索concept接口。每個類型的ctable只需一份。
                對象本體可以很容易地同concept表分離,在完全靜態的情況下,concept表是不需要的。如果需要runtime多態,加上concept表即可。

            posted @ 2007-12-06 17:20 longshanks 閱讀(4232) | 評論 (12)編輯 收藏

            久久99久久成人免费播放| 久久中文字幕人妻熟av女| 久久99精品久久久久久噜噜| 久久精品成人| 国产69精品久久久久99尤物| 久久精品国产99久久无毒不卡| 2021久久精品免费观看| 漂亮人妻被黑人久久精品| 亚洲人成网亚洲欧洲无码久久| 国产色综合久久无码有码| 国产精品一久久香蕉产线看| 一级a性色生活片久久无| 天堂无码久久综合东京热| 久久播电影网| 97久久综合精品久久久综合| 亚洲欧洲精品成人久久奇米网| 国产精品一区二区久久精品| 成人午夜精品无码区久久| 久久久久久A亚洲欧洲AV冫| 东京热TOKYO综合久久精品| 久久夜色精品国产亚洲| 精品久久久久国产免费| 久久综合综合久久综合| 欧美激情一区二区久久久| 久久综合色之久久综合| 亚洲乱码精品久久久久..| 久久人搡人人玩人妻精品首页| 色综合久久综合网观看| 1000部精品久久久久久久久| 亚洲中文字幕久久精品无码喷水| 人人狠狠综合久久亚洲高清| 久久AⅤ人妻少妇嫩草影院| 亚洲一本综合久久| 久久久久国产精品嫩草影院| 久久精品aⅴ无码中文字字幕不卡| 久久天天躁狠狠躁夜夜躁2014| 久久亚洲天堂| 日韩精品久久久久久久电影| 国产精品美女久久久m| 97久久精品无码一区二区天美| 日产精品久久久一区二区|