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

这是本系列的第四篇,终于我们要开始写一些有实际意义的东西了。这一节我们将介绍一些比较难看懂的宏的惯用法(黑魔法),本来准备一口气同时介绍如何实现图灵完备的宏的,结果发现篇幅太长。图灵完备宏放在下一篇中介绍了。那么就让我们开始学习真正的奇技淫巧吧!(๑•̀ㅂ•́)و✧

Token 粘贴

首先我们来复习下上一节的一个例子,这也是我们今天要介绍的第一个惯用法,token 粘贴的直接展开。

1
2
3
4
5
6
7
8
#define FOO_ 0
#define FOO_1 1
#define PRIMITIVE_CAT(x, y) x ## y
#define CAT(x, y) PRIMITIVE_CAT(x, y)
CAT(FOO_, 1)
-> PRIMITIVE_CAT(0, 1)
-> 01

这里如过直接调用 ## 由于宏本身语言的限制 FOO_ 并不会直接展开。而间接的(多加一层调用)调用后就能够克服宏本身的语言限制。后续我们将会看到,间接调用是宏中非常常见的一个技巧。

括号表达式

括号表达式这个名字是我自己起的。准确的叫法并不是很清楚,但是在宏当中却很常用。括号表达式指的是,将参数用括号括起来使用。这样利用 func-like 的宏不接括号不会被展开的特性可以完成一些有意思的东西。比如:

1
2
3
4
5
6
7
8
9
#define EXPAND_IF_PAREN(x) EXPAND x
#define EAT(x)
EXPAND_IF_PAREN((12))
-> EAT (12)
->
EXPAND_IF_PAREN(12)
-> EAT 12

这个例子中如果参数带括号就会返回一个空。当然这个例子是没有什么意义的,但是在下边的例子中你会发现这一技巧将被反复的使用。

模式匹配

利用 Token 粘贴我们能够动态的创建不同的宏名。相当于我们可以描述一个宏名的结构,这和编程语言里的模式匹配正好不谋而合。我们可以利用这个特性我们可以实现类似 if 语句或者 switch 语句的功能。例如:

1
2
3
4
5
6
7
8
9
10
11
#define IIF(c) PRIMITIVE_CAT(IIF_, c)
#define IIF_0(t, ...) __VA_ARGS__
#define IIF_1(t, ...) t
#define IF_TRUE() printf("In true branch");
#define IF_FALSE() printf("In false branch");
IIF(cond) ( \
IF_TRUE(), /*comma needed*/ \
IF_FALSE() \
)

整个过程相当于 构造了不同的宏名,如果为 c 值为0就动态生成IIF_0,反之亦然。相当于变相决定了宏的展开方向。此时如果 cond 等于 1 则执行第一个参数,如果等于0 则执行第二个参数。注意这里用了 PRIMITIVE_CAT 这个宏,因为我们一般希望参数能够完全展开。

此外同样的结构还能玩一些不同的小花样,比如取补

1
2
3
4
5
6
7
8
#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)
#define COMPL_0 1
#define COMPL_1 0
COMPL(1)
-> 0
COMPL(0)
-> 1

检测

在阅读很多库文件的源代码时(比如 boost),我们总会看到很多 XXX_CHECKXXX_PROBE之类的宏。我第一次见到的时候非常的懵逼。从命名上根本看不出来作用是什么,而且还很难找出个准确的关键字去搜索。后来才明白这是一种宏中的惯用法,叫做「检测」(detection)。

检测给了我们这样一种能力,检测某个参数是否是特定的值。众所周知,在宏的基本语法中是不存在 if 这种东西的(宏不是预处理的#if)。但是根据不同的参数展现出不同的行为又是一个很常见的需求。因此,拥有判断某个参数是否是特定值的能力会给我们带来极大的便利。我们先来看看「检测」的写法:

1
2
3
4
5
6
7
8
9
10
11
12
#define GET_SEC(x, n, ...) n
#define CHECK(...) GET_SEC(__VA_ARGS__, 0)
#define PROBE(x) x, 1
CHECK(PROBE())
-> CHECK(x, 1,)
-> GET_SEC(x, 1, 0)
-> 1
CHECK(sth_not_empty)
-> GET_SEC(sth_not_empty, 0)
-> 0

可以看到这个技巧主要是利用了宏的可变长参数的特性。如果有 PROBE的调用 则第二个参数变为1,否则第二个参数保持不变为0。

具体应用的例子,比如可以检测参数是否为空:

1
2
3
4
5
6
7
8
9
10
11
#define IS_EMPTY(x) CHECK(CAT(PRIMITIVE_CAT(IS_EMPTY_, x), 0))
#define IS_EMPTY_0 PROBE()
IS_EMPTY()
-> CHECK(IS_EMPTY_0)
-> CHECK(PROBE())
-> 1
IS_EMPTY(param)
-> CHECK(IS_EMPTY_param_0)
-> 0

如果 x 为空则能够匹配到IS_EMPTY_0调用带有 PROBE() 的宏,否则就只是一串没有意义的字符串而已。利用一个小技巧就能够很方便的实现检测参数是否为空。注意此处我们使用了CATPRIMITIVE_CAT这是因为一般我们期望被判断参数应该被完全展开。

再比如,检测参数是否是括号表达式:

1
2
3
4
5
6
7
8
9
10
#define IS_PAREN(x) CHECK(IS_PAREN_PROBE x)
#define IS_PAREN_PROBE(...) PROBE()
IS_PAREN(())
-> CHECK(IS_PAREN_PROBE ())
-> CHECK(PROBE())
-> 1
IS_PAREN(xxx)
-> CHECK(IS_PAREN_PROBE xxx)
-> 0

我们还可以利用这个技巧实现取反(C 语言中除了0取反是1其他的取反都是0)。

1
2
3
4
5
6
7
#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))
#define NOT_0 PROBE()
NOT(das)
-> 0
NOT(0)
-> 1

然后是转换为布尔值和 if 条件判断,限于篇幅就不推导了,请读者自行尝试:

1
2
3
4
5
6
7
8
9
#define BOOL(x) COMPL(NOT(x))
#define IF(c) IIF(BOOL(c))
IF(1)
-> 1
IF(0)
-> 0
IF(12)
-> 1

这一节到此结束。本节从这里借了很多例子Cloak。下一节介绍如何实现图灵完备的宏。