青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

longshanks

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

常用鏈接

留言簿(10)

我參與的團(tuán)隊(duì)

搜索

  •  

最新評論

閱讀排行榜

評論排行榜

2008年9月18日 #

三只小豬
莫華楓

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

    最近,冷不丁地接到公司下派的一個(gè)緊急任務(wù),做手持POS和PC程序之間交換數(shù)據(jù)的程序。各種各樣的麻煩中,有一個(gè)小得不起眼的問題,POS機(jī)中數(shù)據(jù)的字 節(jié)序和PC相反。這可不算是什么啊。沒錯(cuò),是在太簡單了。盡管如此,還是引發(fā)了一場爭論。做POS程序的,希望PC程序做轉(zhuǎn)換。做PC程序的,希望POS 程序做轉(zhuǎn)換。(誰都想少做點(diǎn),對吧;))。最終,作為和事佬的我,為了維護(hù)和諧的氛圍,攬下了這件事。當(dāng)然,到底在那里做,還是要決定的。最終選擇在PC 上,畢竟PC上調(diào)試起來容易。(天煞的,這個(gè)POS機(jī)沒有debug,也沒有模擬器,顯示屏還沒我手機(jī)的大,做起來著實(shí)費(fèi)事)。
    其實(shí),我的本意是想在POS上做這個(gè)轉(zhuǎn)換。因?yàn)镻OS用的是C(一個(gè)不知什么年代的gcc),可以直接操作字節(jié)。基本的代碼看起來差不多應(yīng)該是這樣:
        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是數(shù)據(jù)類型的字節(jié)長度。這里使用了最長的無符號(hào)整數(shù)類型。這是核心轉(zhuǎn)換函數(shù),各種類型的轉(zhuǎn)換函數(shù)都可以從中派生:
        long InvDataLong(long val) {
            return (long)InvData((unsigned long)val, sizeof(val));
        }
        short InvDataShort(short val) {
            return (short)InvData((unsigned short)val, sizeof(val));
        }
        ...
    最后,有一個(gè)比較特殊的地方,float。float的編碼不同于整型,如果直接用(unsigned long)強(qiáng)制類型轉(zhuǎn)換,只會(huì)把float數(shù)值的整數(shù)部分賦予參數(shù),得不到正確的結(jié)果。正確的做法,應(yīng)當(dāng)是把float占用的四個(gè)字節(jié)直接映射成一個(gè) unsigned long:
        float InvDataFloat(float val) {
            float val=InvData(*(unsigned long*)(&val), sizeof(val));
            return *(float*)(&val);
        }
    通過將float變量的地址強(qiáng)制轉(zhuǎn)換成unsigned long*類型,然后再dereference成unsigned long類型。當(dāng)然還有其他辦法,比如memcpy,這里就不多說了。至于double類型,為了簡化問題,這里將其忽略。如果有64位的整型,那么 double可以采用類似的解法。否則,就必須寫專門的處理函數(shù)。
    當(dāng)然,最終我還是使用C#寫這個(gè)轉(zhuǎn)換。相比之下,C#的轉(zhuǎn)換代碼顯得更具現(xiàn)代風(fēng)味。基本算法還是一樣:
        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_;
        }
    對于習(xí)慣于C/C++的同學(xué)們注意了,long/ulong在C#中不是4字節(jié),而是8字節(jié)。也就是C/C++中的longlong。以這個(gè)函數(shù)為基礎(chǔ),其它整數(shù)類型的字節(jié)序轉(zhuǎn)換也就有了:
        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,出現(xiàn)了麻煩。在C#中,沒有指針,無法象C那樣將float展開成ulong。(unsafe代碼可以執(zhí)行這類操作,但這不是C#嫡親的特性,并且是違背C#設(shè)計(jì)理念的。這里不做考慮)。C#提供了另一種風(fēng)格的操作:
        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_;
        }
    這個(gè)做法盡管有些累贅,但道理上很簡單:把float變量轉(zhuǎn)換成一個(gè)字節(jié)流,然后把相應(yīng)的位置對調(diào),就獲得了字節(jié)反序的float。相比C的float轉(zhuǎn) 換,C#明顯不夠簡練。原因很簡單,C#根本不是用來干這個(gè)的。C是一種非常底層的語言,它的內(nèi)存模型是完全直觀的,與硬件系統(tǒng)相對應(yīng)的。因而,對于這種 與機(jī)器相關(guān)的操作,當(dāng)然也顯得游刃有余。而C#定位于高層開發(fā)的高級語言,底層的內(nèi)存模型是被屏蔽的,程序員無需知道和關(guān)心。
    不過,C#的代碼卻擁有它的優(yōu)勢。只需看一眼這些函數(shù)的使用代碼,便不言自明了:
        //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代碼中,對于不同的類型,需要使用不同命名的函數(shù)。而在C#代碼中,則只需使用DataInv這樣一個(gè)函數(shù)名。至于屆時(shí)選用那個(gè)版本的函數(shù),編譯器會(huì) 根據(jù)實(shí)際的類型自動(dòng)匹配。C#運(yùn)用函數(shù)重載這個(gè)特性,使得調(diào)用代碼可以采用統(tǒng)一的形式。即便是數(shù)據(jù)的類型有所變化,也無需對調(diào)用代碼做任何修改。(這在我 的開發(fā)過程中的得到了驗(yàn)證,傳輸數(shù)據(jù)的成員類型曾經(jīng)發(fā)生變化,我也只是修改了數(shù)據(jù)結(jié)構(gòu)的定義,便將問題搞定)。這一點(diǎn),在C中是無法做到的。
    歸結(jié)起來,C由于側(cè)重于底層,在數(shù)據(jù)轉(zhuǎn)換方便的靈活性,使得轉(zhuǎn)換代碼的構(gòu)建更加容易。而C#則得益于函數(shù)重載,在轉(zhuǎn)換代碼使用方面,有獨(dú)到的優(yōu)勢。
    迄今為止,三只小豬中,還只有兩只出現(xiàn)。下面就要第三只出場了。
    作為C++的粉絲,我會(huì)自然而然地想到使用C++來實(shí)現(xiàn)這個(gè)轉(zhuǎn)換功能。于是便有了如下的代碼:
       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#的函數(shù)重載,兼有兩者的優(yōu)點(diǎn)。
    不過,還能做得更好:
        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;
        }
    這樣,就把所有的整型都一網(wǎng)打盡了,僅用一個(gè)函數(shù)模板,便完成了原先諸多函數(shù)所做的工作。而float版本的函數(shù)則保持不變,作為InvData()的一個(gè)重載。按照C++的函數(shù)模板-重載規(guī)則,float版的函數(shù)重載將被優(yōu)先使用。

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

附:第三只小豬的其他手段:
1、強(qiáng)制轉(zhuǎn)換成字符串?dāng)?shù)組
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、使用標(biāo)準(zhǔn)庫,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去掉其中的循環(huán)。在C++中,這類任務(wù)的實(shí)現(xiàn)方法,完全看程序員的想象力了。:)
posted @ 2008-09-18 19:25 longshanks 閱讀(1961) | 評論 (3)編輯 收藏

2008年8月2日 #

GP技術(shù)的展望——C--
莫華楓

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

C和C++

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

舍棄

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

舍棄之后

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

其他

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

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

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

總結(jié)

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

2008年7月26日 #

GP技術(shù)的展望——先有鴻鈞后有天

莫華楓


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

泛型和類型

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

Concept和類型

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

Concept、類型和對象

    事情還可以有更加夸張的發(fā)展。前面對泛型進(jìn)行了特化,能不能對類型也來一番“特化”呢?當(dāng)然可以:
    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
    實(shí)際上,這無非是在參數(shù)聲明之后加上一組約束條件,用以表明該版本函數(shù)的選擇條件。沒有約束的函數(shù)版本在沒有任何約束條件匹配的情況下被選擇。對于使用立 即數(shù)或者靜態(tài)對象的調(diào)用而言,函數(shù)的選擇在編譯期執(zhí)行,編譯器根據(jù)條件直接調(diào)用匹配的版本。對于變量作為實(shí)參的調(diào)用而言,則需要展開,編譯器將自動(dòng)生成如 下代碼:
    //首先將函數(shù)重新命名,賦予唯一的名稱
    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
    //然后構(gòu)造分派函數(shù)
    void fun_d(int a) {
        if(a==0)
            fun_2(a);
        else if(a>200)
            fun_3(a);
        ...
        else
            fun_1(a);
    }
    在某些情況下,可能需要對一個(gè)對象的成員做出約束,此時(shí)便可以采用這種形式:
    struct A
    {
        float x;
    };
    ...
    void fun(A a:a.x>39.7);
    ...
    這種施加在類型上的所謂“特化”實(shí)際上只是一種語法糖,只是由編譯器自動(dòng)生成了分派函數(shù)而已。這個(gè)機(jī)制在Haskell等語言中早已存在,并且在使用上帶 來很大的靈活性。如果沒有這種機(jī)制,那么一旦需要增加函數(shù)分派條件,那么必須手工修改分派函數(shù)。如果這些函數(shù),包括分派函數(shù),是第三方提供的代碼,那么修 改將是很麻煩的事。而一旦擁有了這種機(jī)制,那么只需添加一個(gè)相應(yīng)的函數(shù)重載即可。
    當(dāng)concept-類型重載和類型-對象重載混合在一起時(shí),便體現(xiàn)更大的作用:
    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-類型-對象重載體系遵循一個(gè)原則:優(yōu)先選擇匹配的函數(shù)中最特化的。這實(shí)際上是類型重載規(guī)則的擴(kuò)展。大的來說,所有類型比所屬的 concept更加特化,所有對象約束比所屬的類型更加特化。對于concept而言,如果concept A refine自concept B,那么A比B更加特化。同樣,如果一個(gè)類型的約束強(qiáng)于另一個(gè),那么前一個(gè)就比后一個(gè)更加特化,比如a==20比a>10更加特化。綜合起來,可以 有這樣一個(gè)抽象的規(guī)則:兩個(gè)約束(concept,或者施加在對象上的約束)A和B,作用在類型或者對象上分別產(chǎn)生集合,如果A產(chǎn)生的集合是B產(chǎn)生的集合 的真子集,那么便認(rèn)為A比B更加特化。
    根據(jù)這些規(guī)則,實(shí)際上可以對一個(gè)函數(shù)的重載構(gòu)造出一個(gè)“特化樹”:

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

類模板

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

從本質(zhì)上講...

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

總結(jié)

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

2008年2月26日 #

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

2008年2月16日 #

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

2008年2月14日 #

瓦格納的排場

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

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

2008年1月25日 #

當(dāng)GPL遇上MP

莫華楓

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

AST宏

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

TMP

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

      expr1 = integer | group;

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

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

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

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

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

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

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

    那么,對于TMP而言,我們感興趣的是它的安全性和相容性。而對其不滿的,則是語法構(gòu)造的靈活性。本著“去其糟粕,取其精華”的宗旨,我們可以對TMP做一番改進(jìn),以獲得更完整的MP技術(shù)。TMP的核心自然是模板(類模板和函數(shù)/操作符模板),在concept的幫助下,模板可以獲得最好的安全性和相容性。以此為基礎(chǔ),如果我們將模板擴(kuò)展到語法構(gòu)造上,那么便可以在保留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);
      }

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

用戶定義的語法

    如果比較帶約束的ast宏和語法模板,會(huì)發(fā)現(xiàn)極其相似。實(shí)際上,兩者殊途同歸,展示了幾乎完全相同的東西。ast宏和TMP分別位于同一個(gè)問題的兩端,當(dāng)它們的缺陷得到彌補(bǔ)時(shí),便會(huì)相互靠攏,最終歸結(jié)到一種形式上。 

    現(xiàn)在,通過結(jié)合兩種方案的特色,我們便可以得到一個(gè)最終的MP構(gòu)造,既安全,又靈活:

      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關(guān)鍵字,在約束(where)的作用下,template和類型參數(shù)列表都已經(jīng)沒有必要了。同時(shí),也允許直接將對象放入 concept:Assignable<x>,這相當(dāng)于:Assignable<decltype<x>>。

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

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

    也可以認(rèn)為,語法構(gòu)造的約束是concept的自然延伸。concept對類型做出約束,而語法構(gòu)造的約束的對象,則是標(biāo)識(shí)符。

    為了強(qiáng)化語法構(gòu)造的創(chuàng)建能力,應(yīng)當(dāng)允許在語法構(gòu)造的定義中使用BNF,或其他類似的語法描述語言。

    在語法構(gòu)造約束的支援下,便可以化解一些子語言同宿主語言之間的語法沖突。比如,我們創(chuàng)建了一種類似spirit的EBNF子語言,可以按照標(biāo)準(zhǔn)的EBNF形式編寫語法,構(gòu)造一個(gè)語法解析器:

      syntax repeat1=p '+'

      where

          p is object of type(Parser)

      {...}

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

      letter + number

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

      int letter, number;

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

      Parser letter, number;

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

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

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

      'x' + 'y'

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

      'x'b_ + 'y'b_

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

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

      where

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

      {...}

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

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

      {

          using namespace XXX;

          ...

      } //XXX的作用范圍不會(huì)超出這個(gè)范圍

    運(yùn)用這個(gè)特性,可以很自然地將蘊(yùn)藏在一個(gè)namespace中的用戶定義語法構(gòu)造限制在一個(gè)確定的范圍內(nèi)。

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

      {

          using namespace XXX exclusive;

          ...

      }

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

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

      {

          using namespace XXX exclusive full;

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

          ...

      }

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

總結(jié)

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

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

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

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

2008年1月6日 #


GP技術(shù)的展望——道生一,一生二

by  莫華楓



    長期以來,我們始終把GP(泛型編程)作為一種輔助技術(shù),用于簡化代碼結(jié)構(gòu)、提高開發(fā)效率。從某種程度上來講,這種觀念是對的。因?yàn)槠駷橹梗珿P技術(shù)還只是一種編譯期技術(shù)。只能在編譯期發(fā)揮作用,一旦軟件完成編譯,成為可執(zhí)行代碼,便失去了利用GP的機(jī)會(huì)。對于現(xiàn)在的多數(shù)應(yīng)用而言,運(yùn)行時(shí)的多態(tài)能力顯得尤為重要。而現(xiàn)有的GP無法在這個(gè)層面發(fā)揮作用,以至于我這個(gè)“GP迷”也不得不灰溜溜地聲稱“用OOP構(gòu)建系統(tǒng),用GP優(yōu)化代碼”。

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

運(yùn)行時(shí)多態(tài)

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

Runtime Concept

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

    下面,就開始第一個(gè)案例。

案例:升級的坦克

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

總結(jié)

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

附錄

Runtime Concept實(shí)現(xiàn)方案二

    我在這篇文章附錄里,給出了一種實(shí)現(xiàn)runtime concept的可能方案。這里,我進(jìn)一步對這個(gè)方案做了一些改進(jìn),使其更加精簡、高效。
    假設(shè)我們有一個(gè)concept:
    concept Shape<T>
    {
        void T::load(xml);
        void T::draw(device);
        void move(T&);
    }
    另外,還有一個(gè)代表圓的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);
    }
    現(xiàn)在有類型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>{}
    當(dāng)我們創(chuàng)建對象時(shí),將會(huì)得到如下圖的結(jié)構(gòu):

runtime concept-2

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

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

2007年12月17日 #

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

by 莫華楓

    公元前216年8月2日,意大利東部平原,一個(gè)叫做坎尼的地方,兩支大軍擺開陣勢,準(zhǔn)備決一死戰(zhàn)。一方是由保羅斯和瓦羅兩位執(zhí)政官率領(lǐng)的羅馬人,另一方則是偉大的軍事天才漢尼拔*巴卡率領(lǐng)的迦太基軍隊(duì)及其同盟。羅馬人超過8萬,而迦太基僅有4萬余人。然而到了傍晚,羅馬人被徹底擊敗,7萬人被殺,僅有少數(shù)得以逃脫。這就是著名的坎尼會(huì)戰(zhàn)。經(jīng)此一役,(外加先前進(jìn)行的特利比亞和特拉西梅諾湖會(huì)戰(zhàn)),羅馬人元?dú)獯髠赡旯駬p失達(dá)五分之一。部分城邦背叛羅馬,西西里也發(fā)生起義。羅馬已經(jīng)到了搖搖欲墜的地步。

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

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

 

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

    Bjarne曾經(jīng)反復(fù)強(qiáng)調(diào),他創(chuàng)建C++為的是將Simular的抽象能力同C的性能結(jié)合起來。于是,在C語言的基礎(chǔ)上,誕生了一種擁有類、繼承、重載等等面向?qū)ο髾C(jī)制的語言。在這個(gè)階段,C++提供了兩個(gè)方面的抽象能力。一種是數(shù)據(jù)抽象,也就是將數(shù)據(jù)所要表達(dá)的含義通過類型以及依附于類型上的成員表述。另一種則是多態(tài),一種最原始的多態(tài)(重載)。

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

    多態(tài)則是更加基礎(chǔ)更加重要的一種特性。多態(tài)使得我們得以用同一種符號(hào)實(shí)現(xiàn)某種確定的語義。多態(tài)的精髓在于:以一種形式表達(dá)一種語義。在此之前,我們往往被迫使用不同的符號(hào)來代表同一種抽象語義,為的是適應(yīng)強(qiáng)類型系統(tǒng)所施加的約束。比如:

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

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

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

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

    重載使得我們只需關(guān)心“加”這個(gè)語義,至于什么類型和什么類型相加,則由編譯器根據(jù)操作數(shù)的類型自動(dòng)解析。

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

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

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

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

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

    但是,如果繼承類覆蓋了基類的虛函數(shù),那么將得到相反的結(jié)果:當(dāng)調(diào)用引用b的fun2,實(shí)際上卻是調(diào)用了D的fun2定義。這表明,覆蓋一個(gè)虛函數(shù),將會(huì)在繼承類和基類之間的所有層次上執(zhí)行覆蓋。這種徹底的、全方位的覆蓋行為,使得我們可以在繼承類上修飾或擴(kuò)展基類的功能或行為。這便是OOP擴(kuò)展機(jī)制的基礎(chǔ)。而這種技術(shù)被稱為動(dòng)多態(tài),意思是基類引用所表達(dá)的語義并非取決于基類本身,而是來源于它所指向的實(shí)際對象,因此它是“多態(tài)”的。因?yàn)橐粋€(gè)引用所指向的對象可以在運(yùn)行時(shí)變換,所以它是“動(dòng)”的。

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

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

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

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

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

    假設(shè)我們有一組函數(shù)執(zhí)行比較兩個(gè)對象大小的操作:

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

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

//代碼#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;
}

    這樣一個(gè)模板可以應(yīng)用于任何類型,不但用一個(gè)符號(hào)表達(dá)了一個(gè)語義,而且用一個(gè)實(shí)現(xiàn)代替了諸多重復(fù)代碼。這便是GP的基本作用。

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

    如果有兩個(gè)指針,分別指向兩個(gè)相同類型的對象。此時(shí)如果我們采用上述compare函數(shù)模板,那么將無法得到所需的結(jié)果。因?yàn)榇藭r(shí)比較的是兩個(gè)指針的值,而不是所指向的對象本身。為了應(yīng)付這種特殊情況,我們需要對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;
}

    這個(gè)“特殊版本”的compare,對于任何類型的指針作出響應(yīng)。如果調(diào)用時(shí)的實(shí)參是一個(gè)指針,那么這個(gè)“指針版”的compare將會(huì)得到優(yōu)先匹配。如果我們將compare改成下面的實(shí)現(xiàn),那么就會(huì)出現(xiàn)非常有趣的行為:

//代碼#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);
}

    當(dāng)我們將指針的指針作為實(shí)參,調(diào)用compare時(shí),神奇的事情發(fā)生了:

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

    compare居然成功地剝離了兩個(gè)指針,并且正確地比較了兩個(gè)對象的值。這個(gè)戲法充分利用了類模板的局部特化和特化解析規(guī)則。根據(jù)規(guī)則,越是特化的模板,越是優(yōu)先匹配。T*版的comp_impl比T版的更加“特化”,會(huì)得到優(yōu)先匹配。那么當(dāng)一個(gè)指針的指針實(shí)例化comp_impl,則會(huì)匹配T*版的 comp_impl,因?yàn)橹羔樀闹羔槪彩侵羔槨*版通過局部特化機(jī)制,剝離掉一級指針,然后用所得的類型實(shí)例化comp_impl。指針的指針剝離掉一級指針,那么還是一個(gè)指針,又會(huì)匹配T*版。T*版又會(huì)剝離掉一級指針,剩下的就是真正可以比較的類型——double。此時(shí),double已無法與 T*版本匹配,只能匹配基礎(chǔ)模板,執(zhí)行真正的比較操作。

    這種奇妙的手法是蘊(yùn)含在模板特化中一些更加本質(zhì)的機(jī)制的結(jié)果。這種意外獲得的“模板衍生產(chǎn)品”可以算作一種編譯時(shí)計(jì)算的能力,后來被一些“好事者”發(fā)展成獨(dú)立的“模板元編程”(Template Meta Programming,TMP)。

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

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

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

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

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

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

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

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

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

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

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

    concept是可以組合的,正式的術(shù)語叫做“refine”。我們可以通過refine進(jìn)一步構(gòu)造出約束更強(qiáng)的concept:

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

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

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

    這個(gè)concept要求類型具備equal成員,但不能有默認(rèn)構(gòu)造函數(shù)。

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

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

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

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

 

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

    漢尼拔的戰(zhàn)略錯(cuò)誤實(shí)際上在從新迦太基城出發(fā)之前已經(jīng)注定。因?yàn)闈h尼拔對羅馬人的遠(yuǎn)征的根本目的并非擊潰并占領(lǐng)羅馬,而是通過打擊羅馬,削弱他們的勢力,瓦解他們的聯(lián)盟。以達(dá)到尋求簽訂和平協(xié)議的目的。然而這種有限戰(zhàn)略卻使導(dǎo)致了他的最終失敗。

 

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

    另一方面,C++最初并未把目標(biāo)定在“創(chuàng)建一種高度抽象,又確保性能的語言”。縱觀C++的發(fā)展,各種抽象機(jī)制并非在完整的規(guī)劃或路線圖的指導(dǎo)下加入語言。所有高級特性都是以“添油戰(zhàn)術(shù)”零打碎敲地加入語言。從某種程度上來看,C++更像是一種實(shí)驗(yàn)性語言,而非工業(yè)語言。C++的強(qiáng)大功能和優(yōu)點(diǎn)是長期積累獲得的,而它的諸多缺陷也是長期添加特性的結(jié)果。

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

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

 

    坎尼會(huì)戰(zhàn)之后,漢尼拔又打過幾次小規(guī)模的勝仗。但經(jīng)過長期的作戰(zhàn),也得不到迦太基的支援,漢尼拔的力量越來越弱,只能在意大利半島上勉強(qiáng)生存。羅馬很快恢復(fù)了元?dú)猓母锪塑娛麦w系和作戰(zhàn)方式,重新掌握了戰(zhàn)略主動(dòng)權(quán)。更重要的是,羅馬也有了自己的“漢尼拔”——(征服非洲的)普布利烏斯·科爾內(nèi)利烏斯·西庇阿(大西庇阿)。西庇阿被派往北非大陸,直接攻擊迦太基人的老巢。漢尼拔被召回,在扎馬與西庇阿擺開陣勢,展開一場決戰(zhàn)。最終,西庇阿運(yùn)用從漢尼拔那里學(xué)到的戰(zhàn)術(shù)擊潰了迦太基人,為羅馬人贏得了第二次布匿戰(zhàn)爭的勝利。

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

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

2007年12月6日 #

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

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

動(dòng)多態(tài)   

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

    class IShape

    {

        virtual void load(xml init)=0;

        virtual void draw(monitor m)=0;

        ...

    };

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

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

        }

    }

    ...

    現(xiàn)在可以使用這些圖形類了:

    vector<IShape*> vg;

    vg.push_back(new Rectangle);

    vg.push_back(new Ellipse);

    ...

    DrawShapes(crt, vg);

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

Runtime Unbound

    Runtime Unbound多態(tài)混合了靜多態(tài)和動(dòng)多態(tài)的特征,即既有類型泛化,又是運(yùn)行時(shí)決斷的。一個(gè)最典型的例子就是ruby的函數(shù):
    class x
       def fun(car)
            car.aboard
        end
    end
    這個(gè)案例非常明確地展示出了Runtime Unbound多態(tài)的特點(diǎn)。car參數(shù)沒有類型,這里也不需要關(guān)心類型,只要求car對象有一個(gè)aboard方法即可。由于ruby是動(dòng)態(tài)語言,能夠運(yùn)行時(shí)檢測對象的特征,并動(dòng)態(tài)調(diào)用對象上的方法。
    在Runtime Unbound的思想指導(dǎo)下,我們利用一種偽造的“動(dòng)態(tài)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繼承,而用關(guān)鍵字anything實(shí)例化vector<>模板。這個(gè)虛構(gòu)的anything關(guān)鍵字所起的作 用就是使得vector能夠接受不同類型的對象。當(dāng)DrawShapes()函數(shù)接收到存放圖形對象的容器后,遍歷每一個(gè)對象,并且調(diào)用對象上的draw ()函數(shù),而不管其類型。
    從這段代碼中,我們可以看出Runtime Unbound多態(tài)帶來的好處。所有圖形類不再需要?dú)w一化成一個(gè)類型(抽象接口)。每個(gè)類只需按照約定,實(shí)現(xiàn)load、draw等成員函數(shù)即可。也就是 說,這些圖形類解耦合了。一旦類型解耦,便賦予我們很大的自由度。最典型的情況就是,我們需要使用一個(gè)其他人開發(fā)的圖形類,并且無法修改其實(shí)現(xiàn)。此時(shí),如 果使用動(dòng)多態(tài),就很麻煩。因?yàn)楸M管這些圖形類都擁有l(wèi)oad、draw等函數(shù),但畢竟不是繼承自IShape,無法直接插入容器。必須編寫一個(gè)繼承自 IShape的適配器,作為外來圖形類的包裝,轉(zhuǎn)發(fā)對其的訪問。表面上,我們只是減少一個(gè)接口的定義,但Runtime Unbound多態(tài)帶來的解耦有著非凡的意義。因?yàn)轭愸詈鲜冀K是OOP設(shè)計(jì)中的一個(gè)令人頭痛的問題。在后面,我們還將看到建立在Runtime Unbound多態(tài)基礎(chǔ)上的更大的進(jìn)步。
    然而,盡管Runtime Unbound多態(tài)具有這些優(yōu)點(diǎn),但因?yàn)榻⒃趧?dòng)態(tài)語言之上,其自身存在的一些缺陷使得這項(xiàng)技術(shù)無法廣泛使用,并進(jìn)入主流。
    Runtime Unbound多態(tài)面臨的第一個(gè)問題就是類型安全。確切的講是靜態(tài)類型安全。
    本質(zhì)上,Runtime Unbound多態(tài)(動(dòng)態(tài)語言)并非沒有類型安全。當(dāng)動(dòng)態(tài)語言試圖訪問一個(gè)未知類型對象的成員時(shí),會(huì)通過一些特殊機(jī)制或特殊接口獲得類型信息,并在其中尋 找所需的對象成員。如果沒有找到,便會(huì)拋出異常。但是,傳統(tǒng)上,我們希望語言能夠在編譯期得到類型安全保證,而不要在運(yùn)行時(shí)才發(fā)現(xiàn)問題。也就是說, Runtime Unbound多態(tài)只能提供運(yùn)行時(shí)類型安全,而無法得到靜態(tài)類型安全。
    第二個(gè)問題是性能。Runtime Unbound需要在運(yùn)行時(shí)搜尋類型的接口,并執(zhí)行調(diào)用。執(zhí)行這類尋找和調(diào)用的方法有兩種:反射和動(dòng)態(tài)鏈接。
    反射機(jī)制可以向程序提供類型的信息。通過這些信息,Runtime Unbound可以了解是否存在所需的接口函數(shù)。反射通常也提供了接口函數(shù)調(diào)用的服務(wù),允許將參數(shù)打包,并通過函數(shù)名調(diào)用。這種機(jī)制性能很差,基本上無法用于稍許密集些的操作。
    動(dòng)態(tài)鏈接則是在訪問對象前在對象的成員函數(shù)表上查詢并獲得相應(yīng)函數(shù)的地址,填充到調(diào)用方的調(diào)用表中,調(diào)用方通過調(diào)用表執(zhí)行間接調(diào)用。這種機(jī)制相對快一些,但由于需要查詢成員函數(shù)表,復(fù)雜度基本上都在O(n)左右,無法與動(dòng)多態(tài)的O(1)調(diào)用相比。
    這些問題的解決,依賴于一種新興的技術(shù),即concept。concept不僅很消除了類型安全的問題,更主要的是它大幅縮小了兩種Runtime多態(tài)的性能差距,有望使Runtime Unbound成為主流的技術(shù)。

concept

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

    ...

    DrawShapes(crt, vg);

    乍看起來沒什么特別的,但是請注意vector<Shape>。這里使用一個(gè)concept,而不是一個(gè)具體的類型,實(shí)例化一個(gè)模板。這里的意思是說,這個(gè)容器接受的是所有符合Shape concept的對象,類型不同也沒關(guān)系。當(dāng)push進(jìn)vg的對象不符合Shape,便會(huì)發(fā)生編譯錯(cuò)誤。

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

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

    如果我們引入這樣一個(gè)規(guī)則:如果用類型創(chuàng)建實(shí)例(對象),那么所創(chuàng)建的對象是靜態(tài)鏈接的,也就是編譯時(shí)鏈接;而用concept創(chuàng)建一個(gè)對象,那么所創(chuàng)建的對象是動(dòng)態(tài)鏈接的,也就是運(yùn)行時(shí)鏈接。

    在這條規(guī)則的作用下,下面這段簡單的代碼將會(huì)產(chǎn)生非常奇妙的效果:

    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;

    };

    在規(guī)則的作用下,m_graph是一個(gè)動(dòng)態(tài)對象,它的類型只有在運(yùn)行時(shí)才能明確。但是無論什么類型,必須滿足Shape concept。而m_n的類型是確定的,所以是一個(gè)靜態(tài)對象

    這和傳統(tǒng)的模板有區(qū)別嗎?模板也可以用不同的類型參數(shù)定義成員數(shù)據(jù)。請看如下代碼:

    Rectangle r;

    Ellipse e;
     nShape(r, 10);

     nShape.setShape(e);   //對于傳統(tǒng)模板而言,這個(gè)操作是非法的,因?yàn)閑和r不是同一種類型

    動(dòng)態(tài)對象的特點(diǎn)在于,我們可以在對象創(chuàng)建后,用一個(gè)不同類型的動(dòng)態(tài)對象代替原來的,只需要這些對象符合相應(yīng)的concept。這在靜態(tài)的模板上是做不到的。

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

運(yùn)行時(shí)concept

   語言層面的concept無法做到這些,因?yàn)樗蔷幾g期機(jī)制。為此,我們需要有一種運(yùn)行時(shí)的concept,或者說二進(jìn)制級別的concept。

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

    當(dāng)一個(gè)“用戶”(函數(shù)或模板)需要在運(yùn)行時(shí)鏈接到對象上的時(shí)候,它會(huì)提交一個(gè)concept的代碼(全局唯一)。系統(tǒng)用這個(gè)代碼在動(dòng)態(tài)對象的 concept表上檢索,獲得指向所需concept的指針,并且填寫到“用戶”給出的一個(gè)“插入點(diǎn)”(一個(gè)指針)中。隨后“用戶”便可以直接通過這個(gè) “插入點(diǎn)”間接調(diào)用所需的函數(shù),成員或自由函數(shù)。

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

    一個(gè)動(dòng)態(tài)對象可以直接用指向concept ctable的指針表示。在不同concept之間轉(zhuǎn)換,相當(dāng)于改變指針的指向,這種操作非常類似OOP中的dynamic_cast。

    對于如下的動(dòng)態(tài)對象定義:

    Shape g=Cycle();

    會(huì)創(chuàng)建一個(gè)Cycle對象,在對象上構(gòu)建起一個(gè)concept表,表中對應(yīng)Cycle所有符合的concept。并且建立一組ctable,每個(gè) ctable對應(yīng)一個(gè)concept。每個(gè)concept表項(xiàng)指向相應(yīng)的ctable。而符號(hào)g則實(shí)際上是指向所建立對象的Shapes ctable的指針。

    對于函數(shù):

    void draw(Shape g);

    draw(g);

    調(diào)用g時(shí),由于draw的參數(shù)是Shape concept,而g正是draw所需的concept,所以無需在對象g的concept表上匹配,可以直接使用這個(gè)ctable指針。這就是說,只要 所用動(dòng)態(tài)對象(g)的concept同使用方(draw函數(shù))能夠匹配,便可以直接使用指向ctable的指針鏈接(編譯時(shí)鏈接),無需在運(yùn)行時(shí)重新匹 配。只有發(fā)生concept轉(zhuǎn)換時(shí),才需要在concept表中搜索,獲得所需的ctable指針:

    Swappable s=g; //Swappable是另一個(gè)concept

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

    所以,Runtime Unbound在運(yùn)行時(shí)concept的作用下,具有同動(dòng)多態(tài)相同的底層行為。因而,他們的性能也是一樣的。很多用于動(dòng)多態(tài)的方案和算法都可以直接用于運(yùn)行時(shí)concept。

Runtime Unbound和Runtime Bound

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

總結(jié)

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

參考

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

附錄 Runtime Concept的具體實(shí)現(xiàn)

    我們有一個(gè)concept:
    concept Shape<T>
    {
        void T::load(xml);
        void T::draw(device);
        void move(T&);
    }
    另外,還有一個(gè)代表圓的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);
    }
    現(xiàn)在有類型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:
        ...
    };
    當(dāng)定義一個(gè)動(dòng)態(tài)對象:
    Shape g=Cycle();
    便會(huì)形成如下圖的結(jié)構(gòu):

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

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

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <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>
            欧美日韩一区二区国产| 亚洲午夜av在线| 欧美成人69av| 亚洲美女淫视频| 亚洲国产精品123| 免费短视频成人日韩| 亚洲裸体在线观看| 在线综合亚洲欧美在线视频| 国产精品免费久久久久久| 欧美一区二区在线视频| 欧美一区网站| 欧美一级片在线播放| 欧美日韩在线不卡| 亚洲精品影视| 亚洲精品免费一区二区三区| 亚洲一区二区三区精品视频| 含羞草久久爱69一区| 久久天天躁夜夜躁狠狠躁2022| 亚洲欧美一级二级三级| 有码中文亚洲精品| 99精品99| 国产亚洲精品aa| 免费在线国产精品| 欧美精品性视频| 亚洲综合二区| 久久午夜电影网| 在线亚洲一区二区| 久久精品国产亚洲精品| 中文在线一区| 久久这里只有| 久久aⅴ乱码一区二区三区| 一区二区三区免费网站| 午夜伦欧美伦电影理论片| 亚洲欧洲一区二区三区在线观看| 亚洲精品国产系列| 亚洲第一福利社区| 亚洲欧美日韩一区| 亚洲午夜成aⅴ人片| 久久一二三四| 久久精品国产2020观看福利| 欧美激情亚洲自拍| 免费欧美电影| 国产精品五月天| 91久久精品一区二区三区| 国产一区二区无遮挡| 亚洲日本成人| 亚洲精品1234| 久久噜噜亚洲综合| 久久精品视频在线看| 欧美午夜在线观看| 日韩视频一区二区三区在线播放免费观看| 一区在线观看| 久久国产色av| 久久精品99无色码中文字幕| 国产精品高清网站| 亚洲精品一区中文| 亚洲免费观看高清在线观看 | 亚洲国产一区二区三区在线播| 国产九九精品视频| 亚洲一区二区精品| 亚洲视频一起| 欧美日韩在线播放| 亚洲美女诱惑| 久久一区二区三区四区| 久久精品中文字幕一区二区三区| 亚洲欧美日韩精品久久奇米色影视 | 久久夜精品va视频免费观看| 国产欧美日韩视频一区二区三区| 中国女人久久久| 亚洲无线一线二线三线区别av| 欧美激情1区2区| 亚洲人成亚洲人成在线观看图片| 亚洲精品国精品久久99热| 欧美高清在线一区| 亚洲另类自拍| 亚洲欧美成人网| 国产精品一区二区三区久久 | 浪潮色综合久久天堂| 精品成人免费| 欧美大片网址| 一区二区冒白浆视频| 亚洲欧美在线视频观看| 国产一区二区精品丝袜| 久久久久9999亚洲精品| 亚洲风情在线资源站| 9国产精品视频| 国产精品theporn88| 午夜一区二区三区不卡视频| 久久亚洲精品网站| 亚洲精选在线| 国产精品一二三视频| 久久久美女艺术照精彩视频福利播放 | 国产精品日韩精品欧美精品| 欧美一区国产一区| 亚洲国产小视频在线观看| 亚洲深夜福利| 国产一区二区欧美日韩| 欧美jizzhd精品欧美喷水 | 美女网站久久| 亚洲视频在线观看免费| 国产色综合天天综合网| 欧美大片国产精品| 午夜亚洲福利| 亚洲电影在线播放| 欧美一区二区三区视频在线| 亚洲国产精品福利| 国产精品美女www爽爽爽| 久久久久久夜| 亚洲一二三四久久| 欧美大片18| 欧美综合77777色婷婷| 99精品热视频只有精品10| 国产亚洲精品aa午夜观看| 欧美日韩在线播放| 免费高清在线一区| 欧美影院成人| 在线亚洲国产精品网站| 亚洲激情啪啪| 欧美成年人网站| 久久精品99国产精品日本| 在线视频你懂得一区二区三区| 国内自拍亚洲| 国产精品视频九色porn| 欧美精品一区二区三区很污很色的| 欧美一级播放| 亚洲一区二区精品在线| 亚洲人精品午夜| 欧美好骚综合网| 亚洲人体1000| 欧美国产极速在线| 久久久久久97三级| 亚洲欧美在线免费| 在线视频亚洲欧美| 日韩视频永久免费观看| 亚洲国产欧美日韩精品| 欧美jizz19性欧美| 久久视频国产精品免费视频在线 | 在线观看欧美亚洲| 国产综合自拍| 曰韩精品一区二区| 尤物在线精品| 亚洲国产高潮在线观看| 在线精品视频一区二区三四| 狠狠爱综合网| 黄色一区二区三区四区| 狠狠色狠狠色综合日日小说| 国产一区二区三区的电影 | 欧美**人妖| 欧美国产日韩亚洲一区| 欧美精品一区在线播放| 欧美日韩国产成人在线观看| 欧美精彩视频一区二区三区| 欧美日韩亚洲高清一区二区| 欧美日韩精品免费观看视频| 欧美日韩久久精品| 国产精品久久7| 国产在线高清精品| 揄拍成人国产精品视频| 亚洲第一精品影视| 日韩一二在线观看| 国产精品99久久久久久白浆小说| 亚洲欧美日韩国产成人精品影院| 午夜精品久久久久久久男人的天堂 | 亚洲男女自偷自拍图片另类| 欧美一区二区三区久久精品茉莉花| 欧美一级播放| 欧美国产高清| 国产麻豆视频精品| 在线日韩中文字幕| 亚洲永久在线观看| 久久久夜精品| 亚洲美女黄网| 亚洲欧美精品suv| 久热精品视频在线观看| 欧美视频在线观看视频极品| 国产婷婷色一区二区三区| 一区免费观看| 亚洲视频1区2区| 老司机成人网| 中文高清一区| 裸体一区二区三区| 国产精品日本欧美一区二区三区| 亚洲大片av| 先锋影音一区二区三区| 欧美高清hd18日本| 欧美一区二区精品| 欧美日韩免费看| 国产曰批免费观看久久久| 一区二区三区四区国产精品| 久久久久久久网站| 在线视频精品一区| 欧美激情二区三区| 国内精品久久久久影院优 | 国产一区二区三区四区三区四| 日韩亚洲欧美一区二区三区| 开心色5月久久精品| 亚洲先锋成人| 欧美日韩国产天堂| 亚洲国产导航| 久久久久综合网|