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
函数里面即可了解思路。