通过C++预处理实现if

C++的预处理机制令人诟病的一点是在不同的编译器中的表现是不同的,在boost/preprocessor/config/config.hpp列举出了非常多的case,所以在本篇文章中我们只考虑g++-7的编译结果。此外,出于便于阅读的考虑,对名字进行了简化,去掉了诸如BOOST_PP等前缀。

一个最简单的原型

我们首先将问题转化为只判断一个bit,0和1。这是trivial的,在boost实现中是著名的IIF宏。

1
2
3
#define IF_BOOL(test, t, f) IF_BOOL_##test(t, f)
#define IF_BOOL_1(t, f) t
#define IF_BOOL_0(t, f) f

接下来我们研究将其他的常量转换为bit。首当其冲的是整数和布尔型,我们可以尽情地复制粘贴。

1
2
3
4
5
6
7
#define BOOL(x) BOOL_##x
#define BOOL_true(x) 1
#define BOOL_false(x) 0
#define BOOL_0(x) 0
#define BOOL_1(x) 1
#define BOOL_2(x) 1
...

然后就是defined和空的判断。对于defined,我们有#if defined这样的语句,不过我们没有一个相应的函数,对于空我们进行了以下的尝试:

1
#define BOOL_(x) EMPTY

经过预处理展开发现结果是BOOL_,因此并不如人意。但是根据SoF,defined问题似乎也可以归结到是否为空上,所以我们来研究如何判空。

判断是否为空

我们需要设计一个IS_EMPTY来判断是否是空

1
2
#define M // return 0
#define N 0 // return 1

boost在boost/preprocessor/facilities/is_empty.hpp中给出一个巧妙的方案

1
2
3
4
5
6
7
8
9
#define STR(x) #x
#define CAT(x, y) x ## y
#define EMPTY()
#define EAT_BRACE(x) x EMPTY

#define IS_EMPTY(x) IS_EMPTY_I(x IS_EMPTY_HELPER)
#define IS_EMPTY_I(contents) BOOST_PP_TUPLE_ELEM(2, 1, (IS_EMPTY_DEF_ ## contents()))
#define IS_EMPTY_DEF_IS_EMPTY_HELPER _, EAT_BRACE(1)
#define IS_EMPTY_HELPER() , 0

这个方案的基本原理是如果我们将IS_EMPTY_DEF_xIS_EMPTY_HELPER三者连接,对于一个平凡情况,如果x为空,那么我们最终会得到一个IS_EMPTY_DEF_IS_EMPTY_HELPER(过程稍后),我们将它展开为_, EAT_BRACE(1)。这是一个奇妙的构造,我们希望的结果1就构造在EAT_BRACE(1)里面,我们将在稍后谋划如何将其取出。为了能够理解代码的精妙之处,我们首先考虑一个不平凡的情况,如果我们做简单的连接,那么可能不能形成一个valid preprocessing token,那么我们就得想办法在早期将它搞掉。现在我们假定x的值就是x,那么我们第一步展开为IS_EMPTY_I(x IS_EMPTY_HELPER)。在第二步,我们不看BOOST_PP_TUPLE_ELEM,它里面的东西是IS_EMPTY_DEF_ ## x IS_EMPTY_HELPER ()。再展开IS_EMPTY_HELPER (),发现它是, 0,于是我们得到了一个IS_EMPTY_DEF_, 0,容易看出我们要的结果同样在第1个(从0开始),我们可以通过BOOST_PP_TUPLE_ELEM将它取出来,其实现将在后面讨论。于是现在我们有了疑问,第一,为什么IS_EMPTY_HELPER后面要加上括号,这样一来怎么合成IS_EMPTY_DEF_IS_EMPTY_HELPER呢?第二,为啥0是裸着的,1要包着一个EAT_BRACE?首先我们推演IS_EMPTY_DEF_ ## IS_EMPTY_HELPER ()得到了这个结果,这时候##可以合成IS_EMPTY_DEF_IS_EMPTY_HELPER这个token,于是IS_EMPTY_HELPER ()就没有被展开。而我们又没有定义IS_EMPTY_DEF_IS_EMPTY_HELPER (),所以最后只能挂着一个_, EAT_BRACE(1) (),而EAT_BRACE的作用当然就是把后面挂着的这个括号去掉啦。

可变参数模板部分

下面我们研究BOOST_PP_TUPLE_ELEM的实现,它位于boost/preprocessor/tuple/elem.hpp。这个宏接受两个或三个参数,当接受三个参数是,前两个分别是size和index,后面的__VA_ARGS__被括号包起来。

1
2
3
4
#define ADD_SIZE_TO_SURFIX(prefix, ...) CAT(prefix, SIZE(__VA_ARGS__))
#define TUPLE_ELEM(...) ADD_SIZE_TO_SURFIX(TUPLE_ELEM_O_, __VA_ARGS__)(__VA_ARGS__)
#define TUPLE_ELEM_O_2(n, tuple) VARIADIC_ELEM(n, BOOST_PP_REM tuple)
#define TUPLE_ELEM_O_3(size, n, tuple) TUPLE_ELEM_O_2(n, tuple)

其中ADD_SIZE_TO_SURFIX对应boost中的BOOST_PP_OVERLOAD方法,其作用是使用SIZE获得__VA_ARGS_的大小,并将这个值放到prefix之后形成新的token。我们看到TUPLE_ELEM视参数个数调用TUPLE_ELEM_O_2TUPLE_ELEM_O_3,而最后都归结为VARIADIC_ELEM。也就是说TUPLE模块的实现借助了VARIADIC模块,TUPLE宏由于有个“重载”,所以它的__VA__ARGS__被用括号括起来,所以要把这一层括号去掉才能调用VARIADIC宏。BOOST_PP_REM定义在boost/preprocessor/tuple/rem.hpp里面,它的实现似乎是用来处理...部分的__VA_ARGS__是空的情况,或者是single element的情况。

获取一个序列的长度

这段构思巧妙地代码来自于boost/preprocessor/variadic/size.hpp

1
2
#define SIZE(...) SIZE_I(__VA_ARGS__, 4, 3, 2, 1,)
#define SIZE_I(e0, e1, e2, e3, e4, size, ...) size

SIZE在调用SIZE_I时,__VA_ARGS__能够填补从e0开始的形参,那么用来标记大小的4, 3, ...的序列就会被往后挤。美中不足的是这个宏不能处理size为0的情况,这是因为空参数表也占一个位置。虽然我们知道对于某些编译期,我们可以使用##__VA_ARGS__去掉前面的逗号,以避免func(non_va_arg1, non_va_arg2, )的情况,但它并不能去掉后面的逗号。事实上我们只能通过编译期来处理。
如果熟悉C++模板,我们可以注意到这种做法很容易对应到std::make_index_sequence之类的做法。

实现ELEM

BOOST_PP_VARIADIC_ELEM定义在boost/preprocessor/variadic/elem.hpp,同样也是运用了和size一样的思路

1
2
3
4
#define VARIADIC_ELEM(n, ...) CAT(VARIADIC_ELEM_, n)(__VA_ARGS__,)
#define VARIADIC_ELEM_0(e0, ...) e0
#define VARIADIC_ELEM_1(e0, e1, ...) e1
#define VARIADIC_ELEM_2(e0, e1, e2, ...) e2

其他的if实现方法

我们在宏里面使用到的if实现方法非常典型,在C中我们可以通过函数数组的方式来实现,类似于我们在宏里面生成了两个函数*_0*_1。在Python中,我们使用and的短路原则来替代三目运算符(虽然我更喜欢T if C then F这样写)。
这里引用知乎上另外一个有趣的方法

1
2
3
4
// 来自知乎 https://www.zhihu.com/question/308901598
true = λ a. λ b. a
false = λ a. λ b. b
if = λ a. λ b. λ c. a(b)(c)

truefalse分别带到if函数里面即可了解思路。