宏定义黑魔法-从入门到奇技淫巧 (6)

这是本系列的最后一篇,介绍一些宏在日常使用中常见的一些坑。 由于宏本质上是对程序的文本进行操作,且宏和 C 语言本身完全是两套系统,所以很多时候总会引入一些奇怪到的错误,这一节我们就来介绍一些常见的需要注意的地方。

不知为什么,似乎在宏的教程中实现最小值宏是某种传统,所以这回我们也以实现一个标准的求最小值的宏为例,进行讲解。

操作符优先级

假设我们定义一个 MIN 宏如下:

1
2
3
4
5
#define MIN(X, Y) X < Y ? X : Y
MIN(2, 3);
-> 2 < 3 ? 2 : 3;
=> 2

该宏正常运行。但是如果我们调用的时候稍微复杂一点:

1
2
3
12 * MIN(2, 3);
-> 12 * 2 < 3 ? 2 : 3;
=> 3

看出问题所在了么?由于算法优先级的问题,所以此处 12 * 2 被先行计算了。导致了错误的结果。解决办法也很简单,就是加括号。

1
#define MIN(X, Y) (X < Y ? X : Y)

但是这样就没有问题了么?并不是,只要遇到相同优先级的运算符还是会出问题,比如我想要比较三个数:

1
2
3
4
5
MIN(2, MIN(3, 4))
-> MIN(2, 3 < 4 ? 3 : 4)
-> 2 < 3 < 4 ? 3 : 4 ? 2 : 3 < 4 ? 3 : 4
=> 1 ? 3 : 4 ? 2 : 3 < 4 ? 3 : 4
=> 3

还是会存在问题。所以比较好的实践是给每一个参数和整体都加上括号。于是我们的 MIN 宏变成了:

1
#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))

拷贝副作用

宏的本质是替换。这就引入一个问题,每一个参数都会在字面上被拷贝一份,并替换到相应的位置。这会导致有副作用的函数被调用多次,例如, 我有一个 count 函数,记录自己被调用的次数,并将次数返回:

1
2
3
4
5
int _count_ = 1;
int count() {
_count_ += 1;
return _count_;
}

那么不同的写法会导致不同的结果,例如:

1
2
3
4
int cur_count = count();
MIN(cur_count, 123);
printf("%d", count()); // 输出 2
1
2
MIN(count(), 123);
printf("%d", count()); // 输出 3

两次的结果居然不一样。这是为什么呢?因为第二次的时候宏展开会变成这个样子:

1
2
MIN(count(), 123);
-> count() < 123 ? count() : 123;

count 在这里被调用了 2 次。处理方法就是我们在这里加入一个临时变量:

1
2
3
4
5
#define MIN(X, Y) ({ \
typeof(X) X_ = (X); \
typeof(Y) Y_ = (Y); \
((X_ < Y_) ? (X_) : (Y_)); \
})

其中({})是一个 GNU 的扩展,它能够使其中的最后一个语句的值作为整个式子的返回值。如果这个扩展不被支持的话……我目前也没什么好办法了。

不过使用临时变量的话就有一个问题,有可能会出现命名冲突的情况。比如在 Clang 中的 MIN 是这么实现的:

1
2
3
4
5
6
#define __NSX_PASTE__(A,B) A##B
#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
#define __NSMIN_IMPL__(A,B,L) ({ \
__typeof__(A) __NSX_PASTE__(__a,L) = (A); \
__typeof__(B) __NSX_PASTE__(__b,L) = (B); \
(__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); })

第一个宏 __NSX_PASTE__ 我们很熟悉了,这就是我们前几节提到的 PRIMITIVE_CAT,之所以分开写是因为保证参数完全展开。第二个宏里边有个 __COUNTER__ 这个宏的意思是是个计数器,每次调用的时候 +1。这样的话,这个宏就很清楚了。其实就是在我们上一个版本的基础上,对每一个变量后边都添加一个数字,比如a12321这种,这样的话就能最大限度的防止明明冲突。正常人也不会用a1234 之类的当做变量名吧。至此这个 MIN 宏就能在大多数情况下正常使用了。那么问题来了,这是 MIN 的最佳实践么?当然不是,真正的最佳实践是这样的:

1
2
3
4
template <typename T>
inline T const& MIN (T const& a, T const& b) {
return a < b ? a:b;
}

吞分号

一般来说我们在调用宏的时候都会在最后加一个分号,就是这个分号会引入新的问题。例如我定义了一个宏用来跳过无用的内存空间:

1
2
3
4
5
6
7
#define SKIP_SPACES(p, limit) \
{ char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; break; }\
}\
}\

为了防止命名冲突,在这里为语句加上了{}。那么如果有人这么写:

1
2
3
4
if(cond)
SKIP_SPACE(p, limit);
else
//something

那么展开后就变成了:

1
2
3
4
5
6
7
8
9
10
11
12
if(cond)
{
char *lim = (limit);
while (p < lim) {
if (*p++ != ' ') {
p--;
break;
}
}
};
else
// something

注意最后的那个小分号。这使得后边的 else 空置了。(其实如果不加{}也会有类似的问题,读者可以想想看。)为了解决这个额外的分号的问题,我们使用一种惯用法do...while(0)

1
2
3
4
5
6
7
#define SKIP_SPACES(p, limit) \
do{ char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; break; }\
}\
}while(0)

注意此处的 while(0) 后边是没有分号的(某大厂高 t 员工就写错了 (๑•̀ㅂ•́)و✧ )编译器一般会把这个多余的一次循环优化掉,所以性能也不会有什么损失。

到这里整个系列就完结了,感谢大家能够看到这里,作为答谢,送大家一个奇技淫巧,如何判断两个指针变量是否同类型:

1
#define CHECK_PTR_OF(type, p) (1 ? p : (type)0)

如果不是同类型这个三元操作符会在编译期间报错,你说这些人是怎么想到的呢 _(:з」∠)_