C宏系统缺陷

c/c++

浏览数:96

2019-3-29

这两天稍稍看了一下boostpreprocessor库,这是一个用C宏写就的库,
发觉boost那帮疯子竟然利用各种奇技淫巧定义出各种数据类型和结构,
包括链表数组等等,还为它们设计了完整的ADT,还有各种各样函数式语言的常见方法,像for_eachfilterconsfold_leftfold_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,若c0则返回y, 若c1则返回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_xxDEC_xx之上定义加法,减法,

这样做相当于需要手工利用最基本的元素构造基本方法,再将这些基本方法不停地复合嵌套,抽象出更高阶的函数,工作量跟创造语言差不多。

本来创造语言还是挺有趣的一件事,可由于刚刚提过的反人类反直觉的古怪传参机制的存在,
致使复合方法构造高阶函数的过程异常痛苦,得不时留意参数展开时会不会被###打断,若被打断则需要增加一层宏来继续展开。

5、无法实现递归,如:

1 #define x y+1
2 #define y x+1

则展开x时,先展开成y+1,继续展开yx+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<<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.hA.h后被include,所以A中的NAMEBNAME覆盖了,结果打印出了B

究其原因,便是C宏定义的所有变量都是全局的,一不小心就会被后面include的头文件修改。

正常的编程语言都会有命名空间词法闭包这种机制来模块化,而这边是C宏所缺乏的。

当然,要避免这种现象也是有办法,就是把模块名作为宏变量的前缀,比如ANAME命名为A_NAMEBNAME命名为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引用到调用方作用域的变量了,而不是它定义所在的作用域