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

            longshanks

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

            常用鏈接

            留言簿(10)

            我參與的團隊

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            當GPL遇上MP

            莫華楓

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

            AST宏

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

            TMP

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

                  expr1 = integer | group;

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

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

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

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

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

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

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

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

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

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

            用戶定義的語法

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

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

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

                  where

                      (x,y is object)

                      && SameType<x, y>

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

                  {

                      std::swap(x, y);

                  }

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

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

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

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

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

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

                  syntax repeat1=p '+'

                  where

                      p is object of type(Parser)

                  {...}

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

                  letter + number

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

                  int letter, number;

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

                  Parser letter, number;

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

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

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

                  'x' + 'y'

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

                  'x'b_ + 'y'b_

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

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

                  where

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

                  {...}

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

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

                  {

                      using namespace XXX;

                      ...

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

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

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

                  {

                      using namespace XXX exclusive;

                      ...

                  }

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

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

                  {

                      using namespace XXX exclusive full;

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

                      ...

                  }

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

            總結

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

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

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

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

            Feedback

            # re: 當GPL遇上MP 2008-01-27 15:20 Magic
            原來GPL還有這個意思,學習了!  回復  更多評論
              

            # re: 當GPL遇上MP 2008-01-27 17:01 WindyWinter
            標題黨  回復  更多評論
              

            # re: 當GPL遇上MP 2008-01-30 09:55 K120
            MP 太過分了吧。  回復  更多評論
              

            # re: 當GPL遇上MP 2008-03-09 16:49 DK_jims
            現在喜歡kiss原則,,,,喜歡笨的  回復  更多評論
              

            久久九九亚洲精品| 久久综合给合久久狠狠狠97色69| 欧美激情一区二区久久久| 久久久久香蕉视频| 久久精品国产99国产精品| 亚洲天堂久久精品| 国产成人无码精品久久久免费 | 国产精品热久久无码av| 91久久精品无码一区二区毛片| 国产午夜久久影院| 久久久91精品国产一区二区三区| 国产精品久久久久久影院 | 国产精品久久久久一区二区三区| 成人久久久观看免费毛片| 久久久久久免费一区二区三区| 国产精品一区二区久久| 99久久精品无码一区二区毛片| 99热成人精品免费久久| 亚洲精品乱码久久久久久不卡| 久久国产精品无| 婷婷国产天堂久久综合五月| 久久人人爽人人爽人人片av麻烦| 一本久久a久久精品vr综合| 国产亚洲精品美女久久久| 久久久中文字幕| 亚洲国产成人久久一区WWW| 亚洲色婷婷综合久久| 999久久久免费国产精品播放| 久久久99精品一区二区| 亚洲中文字幕无码久久综合网| 午夜精品久久久久久毛片| 香港aa三级久久三级| 中文字幕亚洲综合久久菠萝蜜| 久久久久亚洲AV成人片| 国产69精品久久久久99尤物| 久久婷婷五月综合国产尤物app | 久久久国产精品网站| 亚洲Av无码国产情品久久| 久久精品午夜一区二区福利| 精品国产一区二区三区久久蜜臀| 国产美女亚洲精品久久久综合|