C宏系统缺陷
這兩天稍稍看了一下boost的preprocessor庫,這是一個用C宏寫就的庫,
發覺boost那幫瘋子竟然利用各種奇技淫巧定義出各種數據類型和結構,
包括鏈表、棧、數組等等,還為它們設計了完整的ADT,還有各種各樣函數式語言的常見方法,像for_each、filter、cons,fold_left、fold_right之類,
估計這幫人把函數式語言的很多特性搬了上去,
我猜如果不是因為宏展開的深度有限,這個庫估計就是圖靈完備的了.
本著造輪子練本領的原則,我也嘗試自己去實現各種元素,可是智商不夠,越寫越難受,最后無疾而終。
大致總結了一下,暫時發現C的宏有以下反直覺的缺點:
1、無法定義局部變量,所有宏必須在最外層定義,致使全局可見而且沒有類似namespace的功能,命名時超頭疼不支持多行出寫,若要多行需在每行末端加 \
2、無控制流,要實現循環、選擇非常麻煩
3、傳參機制反直覺,正常語言的傳參一般采用應用序,先完全展開參數再傳入,而C的宏參數展開過程中若遇到#或##就停止展開,如:
1 #define BOOL(n) BOOL##n 2 #define BOOL0 0 3 #define BOOL1 1 4 #define BOOL2 1 5 #define BOOL3 1BOOL(n)可獲取值n的真假值
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,
可事與愿違,因為展開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" )
4、缺少整數類型,若要利用計數器循環生成代碼時非常麻煩,首先要自己手工定義一堆整數的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之上定義加法,減法,
這樣做相當于需要手工利用最基本的元素構造基本方法,再將這些基本方法不停地復合嵌套,抽象出更高階的函數,工作量跟創造語言差不多。
本來創造語言還是挺有趣的一件事,可由于剛剛提過的反人類反直覺的古怪傳參機制的存在,
致使復合方法構造高階函數的過程異常痛苦,得不時留意參數展開時會不會被#和##打斷,若被打斷則需要增加一層宏來繼續展開。
5、無法實現遞歸,如:
1 #define x y+1 2 #define y x+1則展開x時,先展開成y+1,繼續展開y,x+1+1,這時又碰到了x,預處理器便停止展開了。
無法實現遞歸,那利用宏實現循環時就變得異常冗長了。
一般來說,while循環和尾遞歸是等價的,所以若支持遞歸,則可用尾遞歸的形式實現循環,但現在不支持,
所以我們需要把尾遞歸的每一步都得親自展開,并將其手工顯示的定義成宏,如:
這樣做不僅麻煩,而且遞歸深度也只能是一個固定值
6、c的宏只是作簡單的文本替換,所以可能會出現替換到文本后語義改變的例子,下面就是一個最經典的例子:
1 #define square(x) x*x 2 cout<<square(2+3)<<endl;替換后就變成了2+3*2+3,所以寫宏時還要注意在必要的地方加括號。。。。。。。。。
這種現象跟SQL注入類似,token層面的替換導致語義發生不合理的改變,比較好的解決方案應該設置一種機制,可以使得開發者能在語法樹層面做替換,因為語義結構的變化容易預判
7、沒辦法傳function-like macro的名字,如:
1 #define ADD(n, m) .......... 2 #define FOR(k, op,...) ......若調用FOR((3,3), ADD,...),想要在FOR內部ADD(3,3),發現預處理器會報錯,說ADD沒定義。
也就是說函數名不能當參數傳入,當然我發現boost里面是可以的,估計是用了什么奇技淫巧,沒耐性看,各位大神知道的話請指點以下。
8、 缺少命名空間,難以模塊化
// 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,但是增加了工作量之余,還降低了可讀性。。
9、 動態作用域,導致不衛生的宏系統
以為宏定義不像普通的函數那樣有自己的環境,宏會直接在調用方的環境中展開,對調用方的作用域造成干擾
如,
// do里面的a屏蔽了調用方作用域的a #define INC(i) do{ int a=0; i++; } while(0) int main() {int a = 1;INC(a); // 期望a=2,然而a依舊是1return 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=4return 0; }結果ADD_A引用到調用方作用域的變量了,而不是它定義所在的作用域
總結
- 上一篇: 判断数据是否服从某一分布(二)——简单易
- 下一篇: appium运行报错java.net.S