C++的预处理机制令人诟病的一点是在不同的编译器中的表现是不同的,在boost/preprocessor/config/config.hpp列举出了非常多的case,所以在本篇文章中我们只考虑g++-7的编译结果。此外,出于便于阅读的考虑,对名字进行了简化,去掉了诸如BOOST_PP等前缀。
一个最简单的原型
我们首先将问题转化为只判断一个bit,0和1。这是trivial的,在boost实现中是著名的IIF宏。
1 |
接下来我们研究将其他的常量转换为bit。首当其冲的是整数和布尔型,我们可以尽情地复制粘贴。
1 |
|
然后就是defined和空的判断。对于defined,我们有#if defined这样的语句,不过我们没有一个相应的函数,对于空我们进行了以下的尝试:
1 |
经过预处理展开发现结果是BOOL_,因此并不如人意。但是根据SoF,defined问题似乎也可以归结到是否为空上,所以我们来研究如何判空。
判断是否为空
我们需要设计一个IS_EMPTY来判断是否是空
1 |
boost在boost/preprocessor/facilities/is_empty.hpp中给出一个巧妙的方案
1 |
这个方案的基本原理是如果我们将IS_EMPTY_DEF_和x和IS_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 |
其中ADD_SIZE_TO_SURFIX对应boost中的BOOST_PP_OVERLOAD方法,其作用是使用SIZE获得__VA_ARGS_的大小,并将这个值放到prefix之后形成新的token。我们看到TUPLE_ELEM视参数个数调用TUPLE_ELEM_O_2或TUPLE_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 |
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 |
其他的if实现方法
我们在宏里面使用到的if实现方法非常典型,在C中我们可以通过函数数组的方式来实现,类似于我们在宏里面生成了两个函数*_0和*_1。在Python中,我们使用and的短路原则来替代三目运算符(虽然我更喜欢T if C then F这样写)。
这里引用知乎上另外一个有趣的方法。
1 | // 来自知乎 https://www.zhihu.com/question/308901598 |
把true和false分别带到if函数里面即可了解思路。