眾多C++書籍都忠告我們C語言宏是萬惡之首,但事情總不如我們想象的那么壞,就如同goto一樣。宏有一個(gè)很大的作用,就是自動(dòng)為我們產(chǎn)生代碼。如果說模板
眾多C++書籍都忠告我們C語言宏是萬惡之首,但事情總不如我們想象的那么壞,就如同goto一樣。宏有
一個(gè)很大的作用,就是自動(dòng)為我們產(chǎn)生代碼。如果說模板可以為我們產(chǎn)生各種型別的代碼(型別替換),
那么宏其實(shí)可以為我們?cè)诜?hào)上產(chǎn)生新的代碼(即符號(hào)替換、增加)。
關(guān)于宏的一些語法問題,可以在google上找到。相信我,你對(duì)于宏的了解絕對(duì)沒你想象的那么多。如果你
還不知道#和##,也不知道prescan,那么你肯定對(duì)宏的了解不夠。
我稍微講解下宏的一些語法問題(說語法問題似乎不妥,macro只與preprocessor有關(guān),跟語義分析又無關(guān)):
1. 宏可以像函數(shù)一樣被定義,例如:
#define min(x,y) (x 但是在實(shí)際使用時(shí),只有當(dāng)寫上min(),必須加括號(hào),min才會(huì)被作為宏展開,否則不做任何處理。
2. 如果宏需要參數(shù),你可以不傳,編譯器會(huì)給你警告(宏參數(shù)不夠),但是這會(huì)導(dǎo)致錯(cuò)誤。如C++書籍中所描
述的,編譯器(預(yù)處理器)對(duì)宏的語法檢查不夠,所以更多的檢查性工作得你自己來做。
3. 很多程序員不知道的#和##
#符號(hào)把一個(gè)符號(hào)直接轉(zhuǎn)換為字符串,例如:
#define STRING(x) #x
const char *str = STRING( test_string ); str的內(nèi)容就是"test_string",也就是說#會(huì)把其后的符號(hào)
直接加上雙引號(hào)。
##符號(hào)會(huì)連接兩個(gè)符號(hào),從而產(chǎn)生新的符號(hào)(詞法層次),例如:
#define SIGN( x ) INT_##x
int SIGN( 1 ); 宏被展開后將成為:int INT_1;
4. 變參宏,這個(gè)比較酷,它使得你可以定義類似的宏:
#define LOG( format, ... ) printf( format, __VA_ARGS__ )
LOG( "%s %d", str, count );
__VA_ARGS__是系統(tǒng)預(yù)定義宏,被自動(dòng)替換為參數(shù)列表。
5. 當(dāng)一個(gè)宏自己調(diào)用自己時(shí),會(huì)發(fā)生什么?例如:
#define TEST( x ) ( x + TEST( x ) )
TEST( 1 ); 會(huì)發(fā)生什么?為了防止無限制遞歸展開,語法規(guī)定,當(dāng)一個(gè)宏遇到自己時(shí),就停止展開,也就是
說,當(dāng)對(duì)TEST( 1 )進(jìn)行展開時(shí),展開過程中又發(fā)現(xiàn)了一個(gè)TEST,那么就將這個(gè)TEST當(dāng)作一般的符號(hào)。TEST(1)
最終被展開為:1 + TEST( 1) 。
6. 宏參數(shù)的prescan,
當(dāng)一個(gè)宏參數(shù)被放進(jìn)宏體時(shí),這個(gè)宏參數(shù)會(huì)首先被全部展開(有例外,見下文)。當(dāng)展開后的宏參數(shù)被放進(jìn)宏體時(shí),
預(yù)處理器對(duì)新展開的宏體進(jìn)行第二次掃描,并繼續(xù)展開。例如:
#define PARAM( x ) x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) );
因?yàn)锳DDPARAM( 1 ) 是作為PARAM的宏參數(shù),所以先將ADDPARAM( 1 )展開為INT_1,然后再將INT_1放進(jìn)PARAM。
例外情況是,如果PARAM宏里對(duì)宏參數(shù)使用了#或##,那么宏參數(shù)不會(huì)被展開:
#define PARAM( x ) #x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) ); 將被展開為"ADDPARAM( 1 )"。
使用這么一個(gè)規(guī)則,可以創(chuàng)建一個(gè)很有趣的技術(shù):打印出一個(gè)宏被展開后的樣子,這樣可以方便你分析代碼:
#define TO_STRING( x ) TO_STRING1( x )
#define TO_STRING1( x ) #x
TO_STRING首先會(huì)將x全部展開(如果x也是一個(gè)宏的話),然后再傳給TO_STRING1轉(zhuǎn)換為字符串,現(xiàn)在你可以這樣:
const char *str = TO_STRING( PARAM( ADDPARAM( 1 ) ) );去一探PARAM展開后的樣子。
7. 一個(gè)很重要的補(bǔ)充:就像我在第一點(diǎn)說的那樣,如果一個(gè)像函數(shù)的宏在使用時(shí)沒有出現(xiàn)括號(hào),那么預(yù)處理器只是
將這個(gè)宏作為一般的符號(hào)處理(那就是不處理)。
我們來見識(shí)一下宏是如何幫助我們自動(dòng)產(chǎn)生代碼的。如我所說,宏是在符號(hào)層次產(chǎn)生代碼。我在分析Boost.
模塊時(shí),因?yàn)樗褂昧舜罅康暮?宏嵌套,再嵌套),導(dǎo)致我壓根沒看明白代碼。后來發(fā)現(xiàn)了一個(gè)小型的模板庫(kù)ttl,說的
是開發(fā)一些小型組件去取代部分Boost(這是一個(gè)好理由,因?yàn)锽oost確實(shí)太大)。同樣,這個(gè)庫(kù)也包含了一個(gè)庫(kù)。
這里的也就是我之前提到的functor。ttl.庫(kù)里為了自動(dòng)產(chǎn)生很多類似的代碼,使用了一個(gè)宏:
#define TTL_FUNC_BUILD_FUNCTOR_CALLER(n) /
template< typename R, TTL_TPARAMS(n) > /
struct functor_caller_base##n /
///...
該宏的最終目的是:通過類似于TTL_FUNC_BUILD_FUNCTOR_CALLER(1)的調(diào)用方式,自動(dòng)產(chǎn)生很多functor_caller_base模板:
template struct functor_caller_base1
template struct functor_caller_base2
template struct functor_caller_base3
///...
那么,核心部分在于TTL_TPARAMS(n)這個(gè)宏,可以看出這個(gè)宏最終產(chǎn)生的是:
typename T1
typename T1, typename T2
typename T1, typename T2, typename T3
///...
我們不妨分析TTL_TPARAMS(n)的整個(gè)過程。分析宏主要把握我以上提到的一些要點(diǎn)即可。以下過程我建議你翻著ttl的代碼,
相關(guān)代碼文件:.hpp, macro_params.hpp, macro_repeat.hpp, macro_misc.hpp, macro_counter.hpp。
so, here we go
分析過程,逐層分析,逐層展開,例如TTL_TPARAMS(1):
評(píng)論