1. 程式人生 > >程式碼自動生成-巨集帶來的奇技淫巧(轉載)

程式碼自動生成-巨集帶來的奇技淫巧(轉載)

關於巨集的一些技巧,總結得挺好,寫庫的時候比較有用。轉自http://www.cppblog.com/kevinlynx/archive/2008/03/19/44828.html眾多C++書籍都忠告我們C語言巨集是萬惡之首,但事情總不如我們想象的那麼壞,就如同goto一樣。巨集有一個很大的作用,就是自動為我們產生程式碼。如果說模板可以為我們產生各種型別的程式碼(型別替換),那麼巨集其實可以為我們在符號上產生新的程式碼(即符號替換、增加)。關於巨集的一些語法問題,可以在google上找到。相信我,你對於巨集的瞭解絕對沒你想象的那麼多。如果你還不知道#和##,也不知道prescan,那麼你肯定對巨集的瞭解不夠。我稍微講解下巨集的一些語法問題(說語法問題似乎不妥,macro只與preprocessor有關,跟語義分析又無關):1. 巨集可以像函式一樣被定義,例如:   #define min(x,y) (x<y?x:y) //事實上這個巨集存在BUG   但是在實際使用時,只有當寫上min(),必須加括號,min才會被作為巨集展開,否則不做任何處理。2. 如果巨集需要引數,你可以不傳,編譯器會給你警告(巨集引數不夠),但是這會導致錯誤。如C++書籍中所描   述的,編譯器(前處理器)對巨集的語法檢查不夠,所以更多的檢查性工作得你自己來做。3. 很多程式設計師不知道的#和##   #符號把一個符號直接轉換為字串,例如:   #define STRING(x) #x   const char *str = STRING( test_string ); str的內容就是"test_string",也就是說#會把其後的符號   直接加上雙引號。   ##符號會連線兩個符號,從而產生新的符號(詞法層次),例如:   #define SIGN( x ) INT_##x   int SIGN( 1 ); 巨集被展開後將成為:int INT_1;

   但有小問題要注意,巨集中遇到#或##時就不會再展開巨集中巢狀的巨集了。什麼意思了?比如使用char *pChar = STRING(__FILE__);雖然__FILE__本身也是一個巨集,但編譯器不會展開它,所以pChar將指向"__FILE__"而不是你要想的形如"D:\XXX.cpp"的原始檔名稱。因此要加一箇中間轉換巨集,先將__FILE__解析成"D:\XXX.cpp"字串。
定義如下所示二個巨集:#define _STRING(x) #x#define STRING(x) _STRING(x)再呼叫下面語句將輸出帶""的原始檔路徑       char* pChar = STRING(__FILE__);       printf("%s %s\n", pChar, __FILE__);可以比較下STRING(__FILE__)與__FILE__的不同,前將帶雙引號,後一個沒有雙引號。

4. 變參巨集,這個比較酷,它使得你可以定義類似的巨集:   #define LOG( format, ... ) printf( format, __VA_ARGS__ )   LOG( "%s %d", str, count );   __VA_ARGS__是系統預定義巨集,被自動替換為引數列表。5. 當一個巨集自己呼叫自己時,會發生什麼?例如:   #define TEST( x ) ( x + TEST( x ) )   TEST( 1 ); 會發生什麼?為了防止無限制遞迴展開,語法規定,當一個巨集遇到自己時,就停止展開,也就是   說,當對TEST( 1 )進行展開時,展開過程中又發現了一個TEST,那麼就將這個TEST當作一般的符號。TEST(1)   最終被展開為:1 + TEST( 1) 。6. 巨集引數的prescan,   當一個巨集引數被放進巨集體時,這個巨集引數會首先被全部展開(有例外,見下文)。當展開後的巨集引數被放進巨集體時,   前處理器對新展開的巨集體進行第二次掃描,並繼續展開。例如:   #define PARAM( x ) x   #define ADDPARAM( x ) INT_##x   PARAM( ADDPARAM( 1 ) );    因為ADDPARAM( 1 ) 是作為PARAM的巨集引數,所以先將ADDPARAM( 1 )展開為INT_1,然後再將INT_1放進PARAM。   例外情況是,如果PARAM巨集裡對巨集引數使用了#或##,那麼巨集引數不會被展開:   #define PARAM( x ) #x   #define ADDPARAM( x ) INT_##x   PARAM( ADDPARAM( 1 ) ); 將被展開為"ADDPARAM( 1 )"。   使用這麼一個規則,可以建立一個很有趣的技術:打印出一個巨集被展開後的樣子,這樣可以方便你分析程式碼:   #define TO_STRING( x ) TO_STRING1( x )   #define TO_STRING1( x ) #x   TO_STRING首先會將x全部展開(如果x也是一個巨集的話),然後再傳給TO_STRING1轉換為字串,現在你可以這樣:   const char *str = TO_STRING( PARAM( ADDPARAM( 1 ) ) );去一探PARAM展開後的樣子。7. 一個很重要的補充:就像我在第一點說的那樣,如果一個像函式的巨集在使用時沒有出現括號,那麼前處理器只是   將這個巨集作為一般的符號處理(那就是不處理)。我們來見識一下巨集是如何幫助我們自動產生程式碼的。如我所說,巨集是在符號層次產生程式碼。我在分析Boost.Function模組時,因為它使用了大量的巨集(巨集巢狀,再巢狀),導致我壓根沒看明白程式碼。後來發現了一個小型的模板庫ttl,說的是開發一些小型元件去取代部分Boost(這是一個好理由,因為Boost確實太大)。同樣,這個庫也包含了一個function庫。這裡的function也就是我之前提到的functor。ttl.function庫裡為了自動產生很多類似的程式碼,使用了一個巨集:#define TTL_FUNC_BUILD_FUNCTOR_CALLER(n)  \ template< typename R, TTL_TPARAMS(n) > \ struct functor_caller_base##n \        ///...該巨集的最終目的是:通過類似於TTL_FUNC_BUILD_FUNCTOR_CALLER(1)的呼叫方式,自動產生很多functor_caller_base模板:template <typename R, typename T1> struct functor_caller_base1template <typename R, typename T1, typename T2> struct functor_caller_base2template <typename R, typename T1, typename T2, typename T3> struct functor_caller_base3///... 那麼,核心部分在於TTL_TPARAMS(n)這個巨集,可以看出這個巨集最終產生的是:typename T1typename T1, typename T2typename T1, typename T2, typename T3///...我們不妨分析TTL_TPARAMS(n)的整個過程。分析巨集主要把握我以上提到的一些要點即可。以下過程我建議你翻著ttl的程式碼,相關程式碼檔案:function.hpp, macro_params.hpp, macro_repeat.hpp, macro_misc.hpp, macro_counter.hpp。so, here we go分析過程,逐層分析,逐層展開,例如TTL_TPARAMS(1):#define TTL_TPARAMS(n) TTL_TPARAMSX(n,T)  => TTL_TPARAMSX( 1, T )#define TTL_TPARAMSX(n,t) TTL_REPEAT(n, TTL_TPARAM, TTL_TPARAM_END, t)=> TTL_REPEAT( 1, TTL_TPARAM, TTL_TPARAM_END, T )#define TTL_TPARAM(n,t) typename t##n, #define TTL_TPARAM_END(n,t) typename t##n#define TTL_REPEAT(n, m, l, p) TTL_APPEND(TTL_REPEAT_, TTL_DEC(n))(m,l,p) TTL_APPEND(TTL_LAST_REPEAT_,n)(l,p)注意,TTL_TPARAM, TTL_TPARAM_END雖然也是兩個巨集,他們被作為TTL_REPEAT巨集的引數,按照prescan規則,似乎應該先將這兩個巨集展開再傳給TTL_REPEAT。但是,如同我在前面重點提到的,這兩個巨集是function-like macro,使用時需要加括號,如果沒加括號,則不當作巨集處理。因此,展開TTL_REPEAT時,應該為:=> TTL_APPEND( TTL_REPEAT_, TTL_DEC(1))(TTL_TPARAM,TTL_TPARAM_END,T) TTL_APPEND( TTL_LAST_REPEAT_,1)(TTL_TPARAM_END,T)這個巨集體看起來很複雜,仔細分析下,可以分為兩部分:TTL_APPEND( TTL_REPEAT_, TTL_DEC(1))(TTL_TPARAM,TTL_TPARAM_END,T)以及TTL_APPEND( TTL_LAST_REPEAT_,1)(TTL_TPARAM_END,T)先分析第一部分:#define TTL_APPEND( x, y ) TTL_APPEND1(x,y) //先展開x,y再將x,y連線起來#define TTL_APPEND1( x, y ) x ## y#define TTL_DEC(n) TTL_APPEND(TTL_CNTDEC_, n)根據先展開引數的原則,會先展開TTL_DEC(1)=> TTL_APPEND(TTL_CNTDEC_,1) => TTL_CNTDEC_1 #define TTL_CNTDEC_1 0  注意,TTL_CNTDEC_不是巨集,TTL_CNTDEC_1是一個巨集。=> 0 , 也就是說,TTL_DEC(1)最終被展開為0。回到TTL_APPEND部分:=> TTL_REPEAT_0 (TTL_TPARAM,TTL_TPARAM_END,T) #define TTL_REPEAT_0(m,l,p)TTL_REPEAT_0這個巨集為空,那麼,上面說的第一部分被忽略,現在只剩下第二部分:TTL_APPEND( TTL_LAST_REPEAT_,1)(TTL_TPARAM_END,T)=> TTL_LAST_REPEAT_1 (TTL_TPARAM_END,T) // TTL_APPEND將TTL_LAST_REPEAT_和1合併起來#define TTL_LAST_REPEAT_1(m,p) m(1,p)=> TTL_TPARAM_END( 1, T )#define TTL_TPARAM_END(n,t) typename t##n=> typename T1  展開完畢。雖然我們分析出來了,但是這其實並不是我們想要的。我們應該從那些巨集裡去獲取作者關於巨集的程式設計思想。很好地使用巨集看上去似乎是一些偏門的奇技淫巧,但是他確實可以讓我們編碼更自動化。參考資料:macro語法: http://developer.apple.com/documentation/DeveloperTools/gcc-4.0.1/cpp/Macros.htmlttl(tiny template library) : http://tinytl.sourceforge.net/ posted on 2013-03-25 17:31
Richard Wei
閱讀(1378) 評論(0)  編輯 收藏 引用 所屬分類: C++