這兩天稍稍看了一下boost
的preprocessor
庫,這是一個用C宏
寫就的庫,
發覺boost
那幫瘋子居然利用各類奇技淫巧定義出各類數據類型和結構,
包括鏈表
、棧
、數組
等等,還爲它們設計了完整的ADT
,還有各類各樣函數式語言的常見方法,像for_each
、filter
、cons
,fold_left
、fold_right
之類,
估計這幫人把函數式語言的不少特性搬了上去,
我猜若是不是由於宏展開的深度有限,這個庫估計就是圖靈完備的了.編程
本着造輪子練本領的原則,我也嘗試本身去實現各類元素,但是智商不夠,越寫越難受,最後無疾而終。數組
大體總結了一下,暫時發現C的宏有如下反直覺的缺點:閉包
一、沒法定義局部變量,全部宏必須在最外層定義
,導致全局可見並且沒有相似namespace
的功能,命名時超頭疼不支持多行出寫,若要多行需在每行末端加 \編程語言
二、無控制流
,要實現循環、選擇很是麻煩模塊化
三、傳參機制反直覺
,正常語言的傳參通常採用應用序,先徹底展開參數再傳入,而C的宏參數展開
過程當中若遇到#
或##
就中止展開,如:函數
1 #define BOOL(n) BOOL##n 2 #define BOOL0 0 3 #define BOOL1 1 4 #define BOOL2 1 5 #define BOOL3 1
BOOL(n)
可獲取值n
的真假值spa
1 #define IF(c, x, y) IF##c(x, y) 2 #define IF0(x, y) y 3 #define IF1(x, y) x
上面的宏是想要實現選擇控制,IF
中傳入邏輯值c
,若c
爲0
則返回y
, 若c
爲1
則返回x
設計
假如按以下調用: IF( BOOL(3), "t", "f" )
,按直覺此句應生成t
,code
可事與願違,由於展開BOOL(3)
時碰到##
,因此直接返回BOOL3
,
結果上面的宏就變成了IF( BOOL3, "t", "f")
,按IF
宏體繼續展開,則變成了 IFBOOL3("t", "f")
最後預處理器報錯: 找不到IFBOOL3
遞歸
所以,爲了能正確地把參數BOOL(3)
展開爲1
,還須要多包裝多一層宏:
1 #define IF(c, x, y) IF_C(c, x, y) 2 #define IF_C(c, x, y) IF##c(x, y)
這樣,IF( BOOL(3), "t", "f" )
就會先展開參數,變成IF( BOOL3, "t", "f")
,
而後宏體展開,IF_C( BOOL3, "t", "f" )
,展開參數成了IF_C( 1, "t", "f" )
,
最後纔會正確地展開成IF1( "t", "f" )
四、缺乏整數類型
,若要利用計數器循環生成代碼時很是麻煩,首先要本身手工定義一堆整數的INC
:
1 #define INC_0 1 2 #define INC_1 2 3 #define INC_2 3 4 #define INC_3 4 5 …………… 6 #define DEC_x x 7 ………………
而後再在INC_xx
和DEC_xx
之上定義加法,減法,
這樣作至關於須要手工利用最基本的元素構造基本方法,再將這些基本方法不停地複合嵌套,抽象出更高階的函數,工做量跟創造語言差很少。
原本創造語言仍是挺有趣的一件事,可因爲剛剛提過的反人類反直覺的古怪傳參機制的存在,
導致複合方法構造高階函數的過程異常痛苦,得不時留意參數展開時會不會被#
和##
打斷,若被打斷則須要增長一層宏來繼續展開。
五、沒法實現遞歸
,如:
1 #define x y+1 2 #define y x+1
則展開x
時,先展開成y+1
,繼續展開y
,x+1+1
,這時又碰到了x
,預處理器便中止展開了。
沒法實現遞歸,那利用宏實現循環時就變得異常冗長了。
通常來講,while
循環和尾遞歸是等價的,因此若支持遞歸,則可用尾遞歸的形式實現循環,但如今不支持,
因此咱們須要把尾遞歸的每一步都得親自展開,並將其手工顯示的定義成宏,如:
1 #define WHLE(...) WHILE##n(...) 2 #define WHILE0(...) xxxxx 3 #define WHLE1(....) WHILE0(......) 4 #define WHLE2(....) WHILE1(......) 5 #define WHLE3(....) WHILE2(......) 6 ...................
這樣作不只麻煩,並且遞歸深度也只能是一個固定值
六、c的宏只是做簡單的文本替換
,因此可能會出現替換到文本後語義改變的例子,下面就是一個最經典的例子:
1 #define square(x) x*x 2 cout<<square(2+3)<<endl;
替換後就變成了2+3*2+3
,因此寫宏時還要注意在必要的地方加括號。。。。。。。。。
這種現象跟SQL注入
相似,token
層面的替換致使語義發生不合理的改變,比較好的解決方案應該設置一種機制,可使得開發者能在語法樹層面作替換,由於語義結構的變化容易預判
七、沒辦法傳function-like macro的名字
,如:
1 #define ADD(n, m) .......... 2 #define FOR(k, op,...) ......
若調用FOR((3,3), ADD,...)
,想要在FOR
內部ADD(3,3)
,發現預處理器會報錯,說ADD
沒定義。
也就是說函數名不能當參數傳入,固然我發現boost
裏面是能夠的,估計是用了什麼奇技淫巧,沒耐性看,各位大神知道的話請指點如下。
八、 缺乏命名空間
,難以模塊化
// A.h #include <stdio.h> #define NAME "A" #define printName() printf(NAME)
// B.h #define NAME "B"
// main.c #include "A.h" #include "B.h" int main() { printName(); return 0; }
通常而言,咱們但願printName()
中的NAME
應該是綁定A.h
裏的NAME
,也就是main.c
應該打印A
,然而,由於B.h
在A.h
後被include
,因此A
中的NAME
被B
的NAME
覆蓋了,結果打印出了B
究其緣由,即是C宏
定義的全部變量都是全局
的,一不當心就會被後面include
的頭文件修改。
正常的編程語言都會有命名空間
、詞法閉包
這種機制來模塊化,而這邊是C宏
所缺少的。
固然,要避免這種現象也是有辦法,就是把模塊名做爲宏變量的前綴
,好比A
的NAME
命名爲A_NAME
,B
的NAME
命名爲B_NAME
,可是增長了工做量之餘,還下降了可讀性。。
九、 動態做用域
,致使不衛生的宏系統
覺得宏定義不像普通的函數那樣有本身的環境,宏會直接在調用方的環境中展開,對調用方的做用域形成干擾
如,
// do裏面的a屏蔽了調用方做用域的a #define INC(i) do{ int a=0; i++; } while(0) int main() { int a = 1; INC(a); // 指望a=2,然而a依舊是1 return 0; }
若是宏是詞法做用域的話,編譯器會進行alpha conversion
更名,INC
裏面的do
內的a
就不會屏蔽掉main
裏面的a
了
還有一例,
int a = 1; // 指望a引用到全局做用域的a,然而卻引用到調用方做用域的a #define ADD_A(i) i + a int main() { int a = 2; int c = ADD_A(a); // 指望c=2+1=3, 然而c=2+2=4 return 0; }
結果ADD_A
引用到調用方做用域的變量了,而不是它定義所在的做用域