这两天稍稍看了一下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 1
BOOL(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
循环和尾递归是等价的,所以若支持递归,则可用尾递归的形式实现循环,但现在不支持,
所以我们需要把尾递归的每一步都得亲自展开,并将其手工显示的定义成宏,如:
1 #define WHLE(...) WHILE##n(...)
2 #define WHILE0(...) xxxxx
3 #define WHLE1(....) WHILE0(......)
4 #define WHLE2(....) WHILE1(......)
5 #define WHLE3(....) WHILE2(......)
6 ...................
这样做不仅麻烦,而且递归深度也只能是一个固定值
6、c的宏只是作简单的文本替换
,所以可能会出现替换到文本后语义改变的例子,下面就是一个最经典的例子:
1 #define square(x) x*x
2 cout<
替换后就变成了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
#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依旧是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
引用到调用方作用域的变量了,而不是它定义所在的作用域