《C++ Template Metaprogramming》附录A——预处理元编程

C++ Template Metaprogramming

附录A:预处理元编程

By David Abraham

(http://www.boost.org/people/dave_abrahams.htm)

By 刘未鹏(pongba)

C++的罗浮宫(http://blog.csdn.net/pongba)

原文链接(http://www.boost-consulting.com/mplbook)

模板元编程是C++中最高阶最抽象,也是最强大的能力。而本章所讲的预处理元编程则与模板元编程相映成趣,相信很多人都没有意识到C/C++预处理器还有这种能力,就像很多人在刚学习C++模板的时候做梦都没有想到还有那么多的衍生技术一样...

动机

即便有模板元编程和Boost Metaprogramming Library的强大的能力可供我们支配,一些C++编程任务仍然需要大量地重复样板式的代码。第五章的tiny_size就是个例子:

template <class T0, class T1, class T2>

struct tiny_size

: int_<3> {};

抛开上面的主模板的模板参数列表中的重复模式[1]不管,下面还有三个偏特化版本,它们更是遵循一个可预期的模式:

template <class T0, class T1>

struct tiny_size<T0,T1,none>

: mpl::int_<2> {};

template <class T0>

struct tiny_size<T0,none,none>

: mpl::int_<1> {};

template <>

struct tiny_size<none,none,none>

: mpl::int_<0> {};

在这种情况下,我们的代码中只有很小一部分具有这种机械味,而我们却要为此重复其余的代码[2],在某些情况下(例如,如果我们实现的是large而非tiny),其余的代码量可能相当可观。当一个模式重复出现两三次或更多时,手动书写就容易导致错误。或许更重要的是,代码会变得难以阅读,因为代码中重要的抽象部分其实正是那个模式,而并非遵循该模式的每个代码片断。

代码生成(Code Generation)

抛开手写吧!机械的代码应该(也的确可以)被机械地生成。对于库的作者,写一个可以生成遵循特定模式的代码片断的程序,然后面临两种选择:一是直接将预生成的源代码文件随库发布,二是发布生成器本身。两者都有缺点。如果客户只得到了预生成的源代码,那么他们就被限制住了——经验表明,这些预生成的代码片断的数目可能今天够用,而明天就不够了!另一方面,如果客户得到了生成程序,那么他们还需要一个可以用来执行该生成程序的程序(例如,解释器),并且,后者必须被整合到build过程中去,除非...

预处理器

...除非生成程序就是预处理元程序[3]!而执行(解释)该元程序的就是C/C++预处理器,尽管它们并非为此目的而设计。用户可以通过#define(代码中)或-D(编译命令行中)来控制代码生成过程。这就避免了上面提到的修改build过程的问题,因为预处理元程序的解释器就是预处理器!例如,我们可以将上面的tiny_size主模板参数化如下:

#include <boost/preprocessor/repetition/enum_params.hpp>

#ifndef TINY_MAX_SIZE

# define TINY_MAX_SIZE 3 // default maximum size is 3

#endif

template <BOOST_PP_ENUM_PARAMS(TINY_MAX_SIZE, class T)>

struct tiny_size

: mpl::int_<TINY_MAX_SIZE>

{};

要测试这个元程序,你可以将编译器切换到预处理模式(使用-E选项),同时确保boost的根目录在#include路径里。例如[4]

g++ -P -E -Ipath/to/boost_1_32_0 -<place w:st="on">I.</place> test.cpp

有了适当的元程序,我们不但可以调整tiny_size的模板参数的个数,还可以调整tiny的最大尺寸——只要#define TINY_MAX_SIZE为恰当的值即可。

Boost Preprocessor Library[MK04]在预处理元编程中充当的角色与MPL在模板元编程中充当的角色类似。它提供了一个高阶构件的framework(例如,BOOST_PP_ENUM_PARAMS),使元编程任务变得容易完成——如果没有这个framework,元编程可能会令人很痛苦:-( 在这个附录中,我们并不去深究预处理器工作的细节或是预处理元编程的一般原则或是BPL库工作的若干细节,而是在一个较高的层次上为你展示这个库,从而让你能够有效地使用它,并且自己探索剩下的部分。

预处理器的基本概念

我们在第二章开始讨论模板元编程——描述了元数据(潜在的模板实参)和元函数(类模板),并在这两个基本概念的基础上构成了对编译期计算的大局观。在本节,我们将以同样的方式来介绍预处理元编程。

这里我们介绍的可能对于你只是一个复习,但是在继续之前,有必要先重申这些基本概念:

预处理标记(Token)

对于预处理器,数据的最基本单元就是预处理标记。预处理标记与你在C++中使用的标识符(identifier),操作符(operator symbol),字面常量(literal)等标记大致对应。从技术上说,预处理标记和正规的标记是有区别的(其细节见C++标准的section 2),但是就目前我们的讨论来说可以暂且忽略。事实上,这里对它们将不作区分。

宏(Macros

宏有两种风格。一种和对象类似:

#define identifier replacement-list

这里identifier是宏的名字,replacement-list是一个或多个tokens的序列。在后面的程序中所有出现identifier的地方都会被展开为replacement-list

另一种是函数风格的宏,它就好比预处理期的元函数

#define identifier(a1, a2, ... an) replacement-list

这里,每一个ai 都代表一个宏形参(parameter)的名字。如果后面的程序中用到了该宏,并给出了适当的实参(argument),那么它将被扩展为replacement-list——其中每次出现宏形参的地方都会被替换为用户给出的宏实参[5]

宏实参(Argument)

定义

宏实参是以下两种预处理标记的非空序列:

1.除逗号或圆括号之外的预处理标记

2.由一对圆括号包围的一集预处理标记

这个定义对预处理元编程有重要影响。注意,首先,下面的两种tokens是特别的:

, ( )

因此,一个宏实参不能包含没有配对的圆括号,或者没有被圆括号包围的逗号。例如,下面的示例代码中,FOO的定义后面的两行代码都是ill-formed

#define FOO(X) X // Unary identity macro

FOO(,) // un-parenthesized comma or two empty arguments

FOO()) // unmatched parenthesis or missing argument

同时还要注意,下面的几种tokens都不是特殊的——预处理器对大括号,方括号,尖括号的配对一无所知:

{ } [ ] < >

所以,下面两行代码是ill-formed

FOO(std::pair<int, long>) // 被解释为以分隔的两个参数

FOO({ int x = 1, y = 2; return x+y; }) // 同上

而如果加上一对冗余的圆括号包围欲传递的参数,代码就正确了:

FOO((std::pair<int,int>)) // one argument

FOO(({ int x = 1, y = 2; return x+y; })) // one argument

但是,由于逗号的特殊含义,所以在不了解一个宏实参包含多少以逗号分隔的标记序列的情况下[6],是不可以随便去掉圆括号的。如果你写了一个宏,并要让它能够接受包含任意多个逗号的宏实参(类似于C里面的可变长参数列表),那么对于使用该宏的用户来说,有两个选择:

1. 将实参用圆括号包围起来,并将其中逗号分隔的token序列的数目作为另一个参数。

2. 将信息编码到一个预处理期的数据结构中去(本章后面会提到)。

BPL库的结构

深入考察BPL库并非本书的范畴,这里我们将给你深入了解BPL工具:你需要使用BPL的电子文档——BOOST_INSTALL/libs/preprocessor目录下的index.htm

打开后,在浏览器的左边你会看到索引,点击其中的“Headers”链接,你会看到整个库的结构。大多数头文件都根据功能被组织在相同的子目录下。顶层的目录仅仅包含一些通用的宏的头文件以及对应每个子目录的头文件(这种头文件仅仅把相应子目录中的头文件都包含进去,例如,boost/preprocessor/selection.hpp包含了selection子目录下的两个头文件max.hpp,min.hpp)。没有对应任何子目录的头文件则声明了一个与文件名同名的宏(有BOOST_PP前缀)。例如,max.hpp声明了BOOST_PP_MAX宏。

你会注意到,通常一个头文件会声明一个额外的宏,它以_D,_R,_Z为后缀[7]。例如,max.hpp中也声明了BOOST_PP_MAX_D宏。在本章中我们会忽略这些宏。如果你想知道它们为何存在以及是如何优化预处理速度的,可以参考电子文档的Topics一节的reentrancy部分。

BPL库的基本概念

在本节中我们将讨论BPL库的基本概念,并分别给出一些简单的例子。

重复

我们可以使用BOOST_PP_ENUM_PARAMS宏生成class T0,class T1,...,class Tn这种(具有特定模式的)重复代码,这符合横向重复的概念。BPL中还有一个纵向重复的概念,我们会在后面介绍。进行横向重复的宏可以在库的repetition子目录下找到。

横向重复

要使用横向重复生成tiny_size的特化版本,我们可以这样写:

#include <boost/preprocessor/repetition.hpp>

#include <boost/preprocessor/arithmetic/sub.hpp>

#include <boost/preprocessor/punctuation/comma_if.hpp>

#define TINY_print(z, n, data) data

#define TINY_size(z, n, unused) /

template <BOOST_PP_ENUM_PARAMS(n, class T)> /

struct tiny_size< /

BOOST_PP_ENUM_PARAMS(n,T) /

BOOST_PP_COMMA_IF(n) /

BOOST_PP_ENUM( /

BOOST_PP_SUB(TINY_MAX_SIZE,n), TINY_print, none) /

> /

: mpl::int_<n> {};

BOOST_PP_REPEAT(TINY_MAX_SIZE, TINY_size, ~)

#undef TINY_size

#undef TINY_print

代码生成从BOOST_PP_REPEAT开始,BOOST_PP_REPEAT是一个高阶的宏,它会重复调用TINY_size宏,也就是它的第二个参数。它的第一个参数指明了重复的次数。而第三个参数可以是任意的,它会被原封不动的传给被调用的宏,这里是TINY_size,而TINY_size并不使用它,所以传递的“~”可以是任意的[8]

TINY_size宏每次被BOOST_PP_REPEAT调用时都会生成一个tiny_size的不同的特化版本。TINY_size宏接受三个参数:

z和前面提到的_Z宏相关。它仅被用于优化的目的。目前我们可以忽略它。

n表示当前为第几次重复。在每次重复调用TINY_size的过程中,n依次为012...

unused,对于这个例子,在每次重复中都是“~”

通常,BOOST_PP_REPEAT会将用户传给它的参数原封不动的转发给被调用的宏(例如,TINY_size)。因为像TINY_size这样的宏的替换文本有好几行,所以除了最后一行,其它行都以反斜杠“/”结尾。其开头的几行调用了BOOST_PP_ENUM来生成以逗号分隔的(模板参数)列表,所以,TINY_size每次被调用都会生成类似下面的代码[9]

template <class T1, class T2, ... class Tn-1>

struct tiny_size<

T1, T2, ... Tn-1

...more...

>

: mpl::int_<n> {};

BOOST_PP_COMMA_IF宏如果接受的参数非零,就会生成一个逗号,否则为空。当n0时,BOOST_PP_ENUM_PARAMS(n,T)什么也不会生成,而紧跟在它后面的BOOST_PP_COMMA_IF(n)也为空,因为“<”后面直接跟“,”是非法的。

再后面,BOOST_PP_ENUM生成TINY_MAX_SIZE-n个以逗号分隔的“none”。除了每次重复都以逗号分隔之外,BOOST_PP_ENUMBOOST_PP_REPEAT没什么区别,所以它的第二个参数(这里是TINY_print)必须和TINY_size的格式相同。在本例中,TINY_print忽略当前重复次数n,而总是简单的将自身替换为第二个参数,也就是“none”

BOOST_PP_SUB实现了token的减法。虽然预处理器本身可以对编译期的算术求值:

#define X 3

...

#if X - 1 > 0 // OK

whatever

#endif

然而预处理元程序却只能操作tokens——认识这一点非常重要。通常,当BPL中的某个宏需要接受一个数值参数时,该数值参数必须以单个token的形式被传递。如果在上面的例子中,我们写的是TINY_MAX_SIZE-n,而不是BOOST_PP_SUB(TINY_MAX_SIZE,n),则BOOST_PP_ENUM的第一个参数就包含了三个tokens3-0(或3-1,或3-2,具体要看是在哪一次重复)。而BOOST_PP_SUB则能够生成单个的token,对于BOOST_PP_SUB(3,0),其生成的是3BOOST_PP_SUB(3,1)2,在后来是1

纵向重复

如果你将前面的例子预编译,则结果会像这样:

template <> struct tiny_size< none , none , none > : mpl::int_<0>

{}; template < class T0> struct tiny_size< T0 , none , none > :

mpl::int_<1> {}; template < class T0 , class T1> struct tiny_size

< T0 , T1 , none > : mpl::int_<2> {};

这些代码全都挤在一行——这就是横向重复的特点。对于某些任务,比如说生成tiny_size主模板,这没有任何问题。但是生成特化版本就不一样了,这至少会导致两个缺点:

1. 如果不将结果代码重新编排,则很难验证我们的元程序做了正确的事情。

2. 嵌套式的横向重复在不同的预编译器下的效率差异较大。对于tiny_size,横向重复生成的每个特化版本都包含另外三个横向重复,其中两个是BOOST_PP_ENUM_PARAMS,还有一个是BOOST_PP_ENUM。如果TINY_MAX_SIZE3,则问题可能不大,但是在目前仍在被使用的预编译器中,至少有一个在TINY_MAX_SIZE达到8的时候会显著的变慢[10]

而解决这些问题的方案自然是纵向重复。纵向重复可以在不同的行生成具有特定模式的代码。BPL提供了两种纵向重复的方式:局部迭代和文件迭代。

局部迭代(Local Iteration

示范局部迭代的最直接的办法就是,在我们的例子中,将对BOOST_PP_REPEAT的调用替换为下面的调用:

#include <boost/preprocessor/iteration/local.hpp>

#define BOOST_PP_LOCAL_MACRO(n) TINY_size(~, n, ~)

#define BOOST_PP_LOCAL_LIMITS (0, TINY_MAX_SIZE - 1)

#include BOOST_PP_LOCAL_ITERATE()

局部迭代会重复调用用户定义的BOOST_PP_LOCAL_MACRO宏,它的参数是迭代指标(iteration index)。因为我们已经定义了TINY_size宏,所以只需让BOOST_PP_MACRO调用它就行了。迭代区间则由BOOST_PP_LOCAL_LIMITS宏给出,该宏必须展开为由括号包围的两个整型,表示一个闭区间,该闭区间内的每个整数被依次传给BOOST_PP_LOCAL_MACRO。注意,这里BOOST_PP_LOCAL_LIMITS的替换式可以包含由多个tokens组成的整型表达式(例如,TINY_MAX_SIZE – 1 就包含了三个tokens),这在BPL中是比较罕见的,这里是其一。

最后要说的是,整个迭代过程是从何时开始的:从上面的代码段的最后一行#include语句开始, BOOST_PP_LOCAL_ITERATOR()是一个宏,它替换为一个文件名,该文件位于BPL库中[11]。你会惊讶的发现,与嵌套式的横向重复相比,许多预处理器处理重复的文件包含更快一些。

现在,如果我们将新的例子扔给预处理器,就会得到下面的结果:

template <> struct tiny_size< none , none , none > : mpl::int_<0>

{};

template < class T0> struct tiny_size< T0 , none , none > : mpl::

int_<1> {};

template < class T0 , class T1> struct tiny_size< T0 , T1 , none

> : mpl::int_<2> {};

以上代码分别位于不同的三行。代码格式得到了较大的改善,但是仍不够理想。随着TINY_MAX_SIZE的增大,验证代码是否符合我们的意思就会变得越来越困难。如果你曾经试图使用调试器单步跟踪一个由宏生成的函数的话,你会了解那是一种多么让人沮丧的经历:调试器停在宏最终被调用的地方(而不是宏定义的地方),所以你无法知道那儿到底生成了什么代码。更糟的是,生成的函数中所有的代码都挤在一行!

文件迭代

显然,样板代码和生成的代码之间的行行对应是调试能力的必要前提。文件迭代方式通过重复包含相同的源文件(样板代码源文件,每次包含生成不同的代码实体)来生成符合某个模式的代码实体。文件迭代对调试能力的影响和C++模板一样:尽管在调试器中样板代码的不同实例看起来重叠在相同的行[12],但是从某种意义上说,我们总算能够单步跟踪函数的源代码了。

要在我们的例子中采取文件迭代,我们可以将前面的局部迭代的代码以及TINY_size的定义换成下面的样子:

#include <boost/preprocessor/iteration/iterate.hpp>

#define BOOST_PP_ITERATION_LIMITS (0, TINY_MAX_SIZE - 1)

#define BOOST_PP_FILENAME_1 "tiny_size_spec.hpp"

#include BOOST_PP_ITERATE()

BOOST_PP_ITERATION_LIMITSBOOST_PP_LOCAL_LIMITS的模式相同,都允许我们提供一个迭代的闭区间。BOOST_PP_FILENAME_1指明了被重复#include的文件名(后面我会为你展示该文件)。后缀_1表示这是进行文件迭代的第一个样板文件[13]——如果我们要在tiny_size_spec.hpp中嵌套地对另一个样板文件进行文件迭代的话,我们应该使用BOOST_FILENAME_2,以免引起混淆。

tiny_size_spec.hpp的内容你应该熟悉,其绝大部分和TINY_size宏的定义是一样的,只不过去掉了每行末尾的“/”

#define n BOOST_PP_ITERATION()

template <BOOST_PP_ENUM_PARAMS(n, class T)>

struct tiny_size<

BOOST_PP_ENUM_PARAMS(n,T)

BOOST_PP_COMMA_IF(n)

BOOST_PP_ENUM(BOOST_PP_SUB(TINY_MAX_SIZE,n), TINY_print, none)

>

: mpl::int_<n> {};

#undef n

每一次迭代,BPL库都会将当前迭代指标(iteration index)通过BOOST_PP_ITERATION()传递给我们,在上面的代码中“#define n BOOST_PP_ITERATION()”将该迭代指标简写n,这可以使样板代码的语法更简洁。注意到我们在tiny_size_spec.hpp里面并没有使用包含哨卫[14],这是因为该头文件要被多次包含并预处理,每次都会由其中的样板代码生成一份不同的代码实体。

现在,预处理结果终于完整保留了样板代码的结构了(见下面的结果代码),对于较大的TINY_MAX_SIZE也容易验证了。例如,当TINY_MAX_SIZE8时,下面的代码摘自GCC预处理阶段的输出:

...

template < class T0 , class T1 , class T2 , class T3>

struct tiny_size<

T0 , T1 , T2 , T3

,

none , none , none , none

>

: mpl::int_<4> {};

template < class T0 , class T1 , class T2 , class T3 , class T4>

struct tiny_size<

T0 , T1 , T2 , T3 , T4

,

none , none , none

>

: mpl::int_<5> {};

...etc.

自迭代

自迭代是比文件迭代更简化的版本,其机理与文件迭代相同。对于文件迭代,我们需要引入一份样板代码,即使这些代码非常简单,也必须创建一个全新的文件(比如,tiny_size_spec.hpp),这明显不够方便。幸运的是,BPL库提供了一个宏,允许我们将样板代码直接放在引发迭代的文件里,换句话说,直接在样板代码文件里引发迭代。实现这种能力的关键是BOOST_PP_IS_ITERATING——如果我们正处在迭代中,BOOST_PP_IS_ITERATING就会扩展为一个非零的值,我们可以利用该值在文件中选择不同的部分——引发迭代的部分或样板代码部分。下面是示范自迭代的完整的tiny_size.hpp文件。特别注意TINY_SIZE_HPP_INCLUDED“包含哨卫的使用以及使用的位置。

#ifndef BOOST_PP_IS_ITERATING

# ifndef TINY_SIZE_HPP_INCLUDED

# define TINY_SIZE_HPP_INCLUDED

# include <boost/preprocessor/repetition.hpp>

# include <boost/preprocessor/arithmetic/sub.hpp>

# include <boost/preprocessor/punctuation/comma_if.hpp>

# include <boost/preprocessor/iteration/iterate.hpp>

# ifndef TINY_MAX_SIZE

# define TINY_MAX_SIZE 3 // default maximum size is 3

# endif

// Primary template

template <BOOST_PP_ENUM_PARAMS(TINY_MAX_SIZE, class T)>

struct tiny_size

: mpl::int_<TINY_MAX_SIZE>

{};

// Generate specializations

# define BOOST_PP_ITERATION_LIMITS (0, TINY_MAX_SIZE - 1)

# define BOOST_PP_FILENAME_1 "tiny_size.hpp" // this file

# include BOOST_PP_ITERATE()

# endif // TINY_SIZE_HPP_INCLUDED

#else // BOOST_PP_IS_ITERATING

# define n BOOST_PP_ITERATION()

# define TINY_print(z, n, data) data

// Specialization pattern

template <BOOST_PP_ENUM_PARAMS(n, class T)>

struct tiny_size<

BOOST_PP_ENUM_PARAMS(n,T)

BOOST_PP_COMMA_IF(n)

BOOST_PP_ENUM(BOOST_PP_SUB(TINY_MAX_SIZE,n), TINY_print, none)

>

: mpl::int_<n> {};

# undef TINY_print

# undef n

#endif // BOOST_PP_IS_ITERATING

更多

关于文件迭代,我们只讲了一小部分。要想获得更多的细节,我们建议你去阅读BPL库中BOOST_PP_ITERATE以及与它相关的宏的电子文档。并且还要注意的是,对于代码重复,没有哪种技术是绝对最好的:你的选择将取决于便利性,可验证性,可调试性,编译速度,以及你自己对逻辑相干性的感觉。

命名协定(Naming Conventions)

注意到TINY_sizeTINY_print在被使用之后立即被#undef了,其间没有任何

#include的头文件。所以它们可以被看作局部的宏定义。因为预处理器无视作用域的存在,所以为了防止名字冲突,仔细选择名字是非常必要的。我们建议使用PREFIXED_lower_case这种形式作为局部宏的名字,而PREFIXED_UPPER_CASE作为全局的。唯一的例外是仅有一个小写字母的名字,你可以用它作为局部宏的名字:没有任何其它头文件会定义一个全局的单字母小写的宏——那是非常糟糕的风格。

算术,逻辑以及比较操作

正如我们前面提到的,BPL预处理库中的许多宏的使用界面都要求参数必须只包含单个token,并且当这些参数被用于数学计算时,直接了当的使用算术表达式是不行的(比如:“A+B*C”是不行的——译者),取而代之的是,BPL使用BOOST_PP_SUB宏来对两个数字token作减法——就像在tiny_size例子中的那样。BPL“/arithmetics/”子目录中包含了一集用于非负数算术运算的宏:

BPL预处理库中的算术运算

表达式

结果

BOOST_PP_ADD(x,y)

x + y

BOOST_PP_DEC(x)

x - 1

BOOST_PP_DIV(x,y)

x / y

BOOST_PP_INC(x,y)

x + 1

BOOST_PP_MOD(x,y)

x % y

BOOST_PP_MUL(x,y)

x * y

BOOST_PP_SUB(x,y)

x - y

“/logical/”子目录则包含以下布尔逻辑运算:

BPL预处理库中的逻辑运算

表达式

结果

BOOST_PP_AND(x,y)

x && y

BOOST_PP_NOR(x,y)

!(x || y)

BOOST_PP_OR(x,y)

x || y

BOOST_PP_XOR(x,y)

(bool)x!= (bool)y?1:0

BOOST_PP_NOT(x)

x ? 0 : 1

BOOST_PP_BOOL(x)

x ? 1 : 0

下面是更具效率的操作,但它们的操作数必须是0或者1(表示一个二进制位(Bit))

表达式

结果

BOOST_PP_BITAND(x,y)

x && y

BOOST_PP_BITNOR(x,y)

!(x || y)

BOOST_PP_BITOR(x,y)

x || y

BOOST_PP_BITXOR(x,y)

(bool)x != (bool)y ? 1 : 0

BOOST_PP_COMPL(x)

x ? 0 : 1

最后,“/comparison/”子目录提供整数(token)比较:

BPL预处理库中的比较操作

表达式

结果

BOOST_PP_EQUAL(x,y)

x == y ? 1 : 0

BOOST_PP_NOT_EQUAL(x,y)

x != y ? 1 : 0

BOOST_PP_LESS(x,y)

x < y ? 1 : 0

BOOST_PP_LESS_EQUAL(x,y)

x <= y ? 1 : 0

BOOST_PP_GREATER(x,y)

x > y ? 1 : 0

BOOST_PP_GREATER_EQUAL(x,y)

x >= y ? 1 : 0

由于通常我们需要在若干可用的比较操作中择其一,所以应该认识到:BOOST_PP_EQUALBOOST_PP_NOT_EQUAL的时间复杂度为O(1)而其它比较操作则通常比较慢一些。

控制结构(control structures)

“/control/”子目录中,BPL库提供了一个BOOST_PP_IF(c,t,f)宏,它所扮演的角色和mpl::if_类似。为了探究控制的含义,我们从一个泛型函数对象框架——Boost.Function——生成代码。boost::function为每种不同参数个数(它所支持的参数个数的上限由一个宏来控制)的函数类型都准备了一份偏特化版本:

template <class Signature> struct function; // primary template

template <class R> // arity = 0

struct function<R()>

definition not shown...

template <class R, class A0> // arity = 1

struct function<R(A0)>

definition not shown...

template <class R, class A0, class A1> // arity = 2

struct function<R(A0,A1)>

definition not shown...

template <class R, class A0, class A1, class A3> // arity = 3

struct function<R(A0,A1,A2)>

definition not shown...

etc....

前面我们已经讲过一些策略可以用于生成上面的代码模式,所以这里就不作冗长的复述了。我们可以使用tiny_size所采用的文件迭代方式:

#ifndef BOOST_PP_IS_ITERATING

# ifndef BOOST_FUNCTION_HPP_INCLUDED

# define BOOST_FUNCTION_HPP_INCLUDED

# include <boost/preprocessor/repetition.hpp>

# include <boost/preprocessor/iteration/iterate.hpp>

# ifndef FUNCTION_MAX_ARITY

# define FUNCTION_MAX_ARITY 15

# endif

template <class Signature> struct function; // primary template

// Generate specializations

# define BOOST_PP_ITERATION_LIMITS (0, FUNCTION_MAX_ARITY)

# define BOOST_PP_FILENAME_1 "boost/function.hpp" //this file

# include BOOST_PP_ITERATE()

# endif // BOOST_FUNCTION_HPP_INCLUDED

#else // BOOST_PP_IS_ITERATING

# define n BOOST_PP_ITERATION()

// Specialization pattern

template <class R BOOST_PP_ENUM_TRAILING_PARAMS(n, class A)>

struct function<R ( BOOST_PP_ENUM_PARAMS(n,A) )>

definition not shown...

# undef n

#endif // BOOST_PP_IS_ITERATING

[上面使用的BOOST_PP_ENUM_TRAILING_PARAMS在它的第一个参数不为0时会生成一个前导“,”,其它方面则跟BOOST_PP_ENUM_PARAMS类似]

实参选择(Argument Selection)

为了实现和C++标准库中的算法的互操作——如果接受一个或两个参数的function可以相应的继承自恰当的std::unary_functionstd::binary_function[15]的话。在处理这些特殊情况时,BOOST_PP_IF是一个有力的工具:

# include <boost/preprocessor/control/if.hpp>

# include <boost/preprocessor/comparison/equal.hpp>

// Specialization pattern

template <class R BOOST_PP_ENUM_TRAILING_PARAMS(n, class A)>

struct function<R ( BOOST_PP_ENUM_PARAMS(n,A) )>

BOOST_PP_IF(

BOOST_PP_EQUAL(n,2), : std::binary_function<A0, A1, R>

, BOOST_PP_IF(

BOOST_PP_EQUAL(n,1), : std::unary_function<A0, R>

, ...empty argument...

)

)

{ ...class body omitted... };

呃,很不幸的是,我们一上来就遇到了问题:首先,你不能把一个空参数(...empty argument...)传给宏。其次,因为尖括号并没有被预处理器特殊对待,所以std::unary_function<A0,R>(binary_function<...>)特化版本中的逗号被看作分隔宏参数的标志,从而预处理器会抱怨我们传递给BOOST_PP_IF的参数个数太多。

让我们先来看内层的BOOST_PP_IF调用。mpl::eval_if的策略(选择一个无参函数来调用(eval,也就是求值))可以运用到这里,BPL中没有与mpl::eval_if类似的宏,但其实这里并不需要类似mpl::eval_if的东西:我们只要在BOOST_PP_IF后面多添加一对括号就可以解决这个问题:

#define BOOST_FUNCTION_unary() : std::unary_function<A0,R>

#define BOOST_FUNCTION_empty() // nothing

...

, BOOST_PP_IF(

BOOST_PP_EQUAL(n,1), BOOST_FUNCTION_unary

, BOOST_FUNCTION_empty

)()

#undef BOOST_FUNCTION_empty

#undef BOOST_FUNCTION_unary

一个什么也不生成的宏通常在很多地方都是有用的,以至于BPLfacilities中就提供了一个现成的:BOOST_PP_EMPTY。好,现在我们可以纠正上面的错误了——我们可以把BOOST_FUNCTION_binary()BOOST_FUNCTION_unary()以及BOOST_PP_EMPTY()的求值一直延迟到外围的BOOST_PP_IF调用(展开)结束后,因为std::binary_function<A0,A1,R>也存在逗号问题

# include <boost/preprocessor/facilities/empty.hpp>

# define BOOST_FUNCTION_binary() : std::binary_function<A0,A1,R>

# define BOOST_FUNCTION_unary() : std::unary_function<A0,R>

// Specialization pattern

template <class R BOOST_PP_ENUM_TRAILING_PARAMS(n, class A)>

struct function<R ( BOOST_PP_ENUM_PARAMS(n,A) )>

BOOST_PP_IF(

BOOST_PP_EQUAL(n,2), BOOST_FUNCTION_binary

, BOOST_PP_IF(

BOOST_PP_EQUAL(n,1), BOOST_FUNCTION_unary

, BOOST_PP_EMPTY

)

)()

{

...class body omitted...

};

# undef BOOST_FUNCTION_unary

# undef BOOST_FUNCTION_binary

# undef n

注意,由于我们碰巧使用了文件迭代,所以我们也可以对n的值直接使用#if

template <class R BOOST_PP_ENUM_TRAILING_PARAMS(n, class A)>

struct function<R ( BOOST_PP_ENUM_PARAMS(n,A) )>

#if n == 2

: std::binary_function<A0, A1, R>

#elif n == 1

: std::unary_function<A0, R>

#endif

BOOST_PP_IF则使我们可以把代码逻辑封装到一个可复用的宏当中去(n为参数),它与所有的重复构造都是一致的:

#define BOOST_FUNCTION_BASE(n) /

BOOST_PP_IF(BOOST_PP_EQUAL(n,2), BOOST_FUNCTION_binary /

, BOOST_PP_IF(BOOST_PP_EQUAL(n,1), BOOST_FUNCTION_unary /

, BOOST_PP_EMPTY /

) /

)()

其它选择性结构

BOOST_PP_IDENTITY也属于“facilities”,它是BOOST_PP_EMPTY的一个镜像

#define BOOST_PP_IDENTITY(tokens) tokens BOOST_PP_EMPTY

你可以把它看成一个返回tokens的无参宏——当在后面加以一对空括号的时候,末尾的BOOST_PP_EMPTY被展开为空,从而只剩下tokens。如果我们想在function为三元以上时让它继承自mpl::empty_base的话,就可以使用BOOST_PP_IDENTITY

// Specialization pattern

template <class R BOOST_PP_ENUM_TRAILING_PARAMS(n, class A)>

struct function<R ( BOOST_PP_ENUM_PARAMS(n,A) )>

BOOST_PP_IF(

BOOST_PP_EQUAL(n,2), BOOST_FUNCTION_binary

, BOOST_PP_IF(

BOOST_PP_EQUAL(n,1), BOOST_FUNCTION_unary

, BOOST_PP_IDENTITY(: mpl::empty_base)

)

)()

{

...class body omitted...

};

还有一个要注意的宏是BOOST_PP_EXPR_IF,它根据其第一个参数(一个布尔值)来决定是否展开为其第二个参数:

#define BOOST_PP_EXPR_IF(c,tokens) /

BOOST_PP_IF(c,BOOST_PP_IDENTITY(tokens),BOOST_PP_EMPTY)()

例如,BOOST_PP_EXPR_IF(1,foo)展开为foo,而BOOST_PP_EXPR_IF(0,foo)则展开为空。

Token粘贴(Token Pasting)

如果能有一个一般的方式来访问所有函数对象的参数类型和返回类型(而不是仅限于一元或二元函数)该多好啊!使用一个元函数(metafunction)将函数签名(Signature)“编码为一个MPL序列是一个可行的方案。我们只需要为每个不同function给出一个signature类模板的特化版本:

template <class F> struct signature; // primary template

// partial specializations for boost::function

template <class R>

struct signature<function<R()> >

: mpl::vector1<R> {};

template <class R, class A0>

struct signature<function<R(A0)> >

: mpl::vector2<R,A0> {};

template <class R, class A0, class A1>

struct signature<function<R(A0,A1)> >

: mpl::vector3<R,A0,A1> {};

...

要生成这些特化版本,只需添加下面的代码样板

template <class R, BOOST_PP_ENUM_TRAILING_PARAMS(n, class A)>

struct signature<function<R( BOOST_PP_ENUM_PARAMS(n,A) )> >

: mpl::BOOST_PP_CAT(vector,n)<

R, BOOST_PP_ENUM_TRAILING_PARAMS(n,A)

> {};

BOOST_PP_CAT实现了Token粘贴——它将其两个参数成单个token。因为这是一个通用的宏,所以它位于cat.hpp(该文件位于BPL目录树的最顶层)中。

尽管预处理器有一个内建的Token粘贴操作符“##”,但是它只能用于宏定义中。如果我们在这里使用了它,则根本不会有任何作用:

template <class R>

struct signature<function<R()> >

: mpl::vector##1<R> {};

template <class R, class A0>

struct signature<function<R(A0)> >

: mpl::vector##2<R,A0> {};

template <class R, class A0, class A1>

struct signature<function<R(A0,A1)> >

: mpl::vector##3<R,A0,A1> {};

...

还有,##经常会导致令人意外的结果——因为它在其实参展开之前就会起作用:

#define N 10

#define VEC(i) vector##i

VEC(N) // vectorN

作为比较,BOOST_PP_CAT则把拼接操作延迟到其所有实参都充分求值(展开)之后:

#define N 10

#define VEC(i) BOOST_PP_CAT(vector,i)

VEC(N) // vector10

数据类型(Data Types)

BPL库还提供了数据类型——你可以把它和MPL的类型序列(type sequence)类似的来看待。BPL的数据类型保存的是宏实参 ,而不是C++类型。

序列

序列(简写为seq)是非空的宏实参串(其中每个宏实参都以小括号括住)。例如,下面是一个含有三个元素的序列:

#define SEQUENCE3 (f(12))(a+1)(foo)

下面就是如何使用序列来生成is_integral模板(Boost.Type Traits)的过程:

template <class T>

struct is_integral<t> : mpl::false_ {};

// A seq of integral types with unsigned counterparts

#define BOOST_TT_basic_ints (char)(short)(int)(long)

// generate a seq containing "signed t" and "unsigned t"

#define BOOST_TT_int_pair(r,data,t) (signed t)(unsigned t)

// A seq of all the integral types

#define BOOST_TT_ints /

(bool)(char) /

BOOST_SEQ_FOR_EACH(BOOST_TT_int_pair, ~, BOOST_TT_basic_ints)

// generate an is_integral specialization for type t

#define BOOST_TT_is_integral_spec(r,data,t) /

template <> /

struct is_integral<t> : mpl::true_ {};

BOOST_SEQ_FOR_EACH(BOOST_TT_is_integral_spec, ~, BOOST_TT_ints)

#undef BOOST_TT_is_integral_spec

#undef BOOST_TT_ints

#undef BOOST_TT_int_pair

#undef BOOST_TT_basic_ints

BOOST_SEQ_FOR_EACH是一个高阶(higher-order)宏,与BOOST_PP_REPEAT类似。它以其第一个参数为函数,以其第三个参数所表现的序列中的各元素依次为实参,进行一次次的调用。

序列是最具效率,最灵活,也是最容易使用的BPL数据类型——前提是你并不需要一个空序列:一个空序列不包含任何token,因此不能被作为宏实参来传递。而这里讲到的其它数据类型都可以为空。

操纵序列的设施都位于“/seq/”子目录中。下表进行了一个总结(其中t代表序列(t0)(t1)...(tk)。而s,r,d跟我们前面提到的z参数的意图是类似的(目前建议你忽略它) )

预处理序列操作

表达式

结果

BOOST_PP_SEQ_CAT(t)

t0t1...tk

BOOST_PP_SEQ_ELEM(n,t)

tn

BOOST_PP_SEQ_ENUM(t)

t0, t1, ...tk

BOOST_PP_SEQ_FILTER(pred,data,t)

t without the elements that don't satisfy pred

BOOST_PP_SEQ_FIRST_N(n,t)

(t0)(t1)...(tn-1)

BOOST_PP_SEQ_FOLD_LEFT(op, x, t)

...op(s, op(s, op(s, x, t0), t1), t2)...

BOOST_PP_SEQ_FOLD_RIGHT(op, x, t)

...op(s, op(s, op(s, x, tk), tk-1), tk-2)...

BOOST_PP_SEQ_FOR_EACH(f, x, t)

f(r, x, t0) f(r, x, t1)...f(r, x, tk)

BOOST_PP_SEQ_FOR_EACH_I(g, x, t)

g(r, x, 0, t0) g(r, x, 1, t1)... g(r, x, k, tk)

BOOST_PP_SEQ_FOR_EACH_PRODUCT(h, x, t)

Cartesian product -

see online docs

BOOST_PP_SEQ_INSERT(t,i,tokens)

(t0)(t1)...(ti-1) (tokens) (ti)(ti+1)...(tk)

BOOST_PP_SEQ_POP_BACK(t)

(t0)(t1)...(tk-1)

BOOST_PP_SEQ_POP_FRONT(t)

(t1)(t2)...(tk)

BOOST_PP_SEQ_PUSH_BACK(t,tokens)

(t0)(t1)...(tk)(tokens)

BOOST_PP_SEQ_PUSH_FRONT(t,tokens)

(tokens)(t0)(t1)...(tk)

BOOST_PP_SEQ_REMOVE(t,i)

(t0)(t1)...(ti-1)(ti+1)...(tk)

BOOST_PP_SEQ_REPLACE(t,i,tokens)

(t0)(t1)...(ti-1)(tokens)(ti+1)...(tk)

BOOST_PP_SEQ_REST_N(n,t)

(tn)(tn+1)...(tk)

BOOST_PP_SEQ_REVERSE(t)

(tk)(tk-1)...(t0)

BOOST_PP_SEQ_HEAD(t)

t0

BOOST_PP_SEQ_TAIL(t)

(t1)(t2)...(tk)

BOOST_PP_SEQ_SIZE(t)

k+1

BOOST_PP_SEQ_SUBSEQ(t,i,m)

(ti)(ti+1)...(ti+m-1)

BOOST_PP_SEQ_TO_ARRAY(t)

(k+1 , (t0, t1,...tk))

BOOST_PP_SEQ_TO_TUPLE(t)

(t0, t1,...tk)

BOOST_PP_SEQ_TRANSFORM(f, x, t)

(f(r, x,t0)) (f(r, x,t1))... (f(r, x,tk))

值得注意的是:虽然序列的长度是没有上限的,但是像BOOST_PP_SEQ_ELEM这样接受数值参数的操作最多只能接受至256的数。

Tuples

tuple是一种非常简单的数据类型,BPL对它提供了随机访问以及一些其它的基本操作。tuple的形式是:以括号括起来的,逗号分隔的宏实参列表。例如,下面是一个含有三个元素的tuple

#define TUPLE3 (f(12),a+1,foo)

BPL“/tuple”子目录下包含对tuple的操作,最多支持256个元素的tuple。例如,一个tuple的第N个元素可以通过BOOST_PP_TUPLE_ELEM来访问,如下:

//length index tuple

BOOST_PP_TUPLE_ELEM( 3 , 1 , TUPLE3) // a+1

注意:我们必须将tuple的长度作为第一个参数传递给BOOST_PP_TUPLE_ELEM。事实上,所有的tuple操作都要求显式指出tuple的长度。还有另外四个tuple操作这里就不作介绍了——你可以从在线文档中搜索更多的细节。然而,我们注意到,序列可以被转换为tuple——通过BOOST_PP_SEQ_TO_TUPLE,而且非空的tuple也可以被转换为序列——通过BOOST_PP_TUPLE_TO_SEQ

tuple最强大的能力是:它的表现形式和宏实参列表是一样的:

#define FIRST_OF_3(a1,a2,a3) a1

#define SECOND_OF_THREE(a1,a2,a3) a2

#define THIRD_OF_THREE(a1,a2,a3) a3

// Uses tuple as an argument list

# define SELECT(selector, tuple) selector tuple

SELECT(THIRD_OF_THREE, TUPLE3) // foo

Array

array是一个tuple——不同的是它的第一个元素是tuple的长度(非负数)

#define ARRAY3 ( 3, TUPLE3 )

因为array“随身携带着它的长度信息,所以BPL中操作array的使用界面与操纵tuple的相比简单多了:

BOOST_PP_ARRAY_ELEM(1, ARRAY3) // a+1

BPL库中的“/array/”子目录中包含操作array的设施——最多支持25个元素的array。下表总结了它们(其中a (k,(a0,a1,...,ak-1))

预处理库中的Array操作

表达式

结果

BOOST_PP_ARRAY_DATA(a)

(a0,a1,... ak-1)

BOOST_PP_ARRAY_ELEM(i,a)

ai

BOOST_PP_ARRAY_INSERT(a, i, tokens)

(k+1, (a0,a1,... ai-1, tokens, ai,ai+1,... ak-1))

BOOST_PP_ARRAY_POP_BACK(a)

(k-1, (a0,a1,... ak-2))

BOOST_PP_ARRAY_POP_FRONT(a)

(k-1, (a1,a2,... ak-1))

BOOST_PP_ARRAY_PUSH_BACK(a, tokens)

(k+1, (a0,a1,... ak-1, tokens))

BOOST_PP_ARRAY_PUSH_FRONT(a, tokens)

(k+1, (tokens, a1,a2,... ak-1))

BOOST_PP_ARRAY_REMOVE(a, i)

(k-1, (a0,a1,... ai-1,ai+1,... ak-1))

BOOST_PP_ARRAY_REPLACE(a, i, tokens)

(k, (a0,a1,... ai-1, tokens, ai+1,... ak-1))

BOOST_PP_ARRAY_REVERSE(a)

(k, (ak-1,ak-2,... a1,a0))

BOOST_PP_ARRAY_SIZE(a)

k

List

list是一个含有两个元素的tuple,其第一个元素是list的第一个元素,而第二个元素是其余元素的list(注意,这是个递归定义——译者),或者BOOST_PP_NIL(如果没有剩余元素的话)list的访问方式和运行期的链表很相似。下面是一个含有三个元素的list

#define LIST3 (f(12), (foo, BOOST_PP_NIL)))

BPL中的“/list/”子目录中包含操纵list的设施。由于这些操作是序列操作的一个子集,我们就不一一罗列了——通过序列操作的文档,不难理解list操作的含义。

和序列类似,list没有固定的长度上限。但是和序列不同的是,list可以为空。通常,在一个预处理数据结构中,你很少需要大于25个元素,并且,跟其它数据结构比起来,list通常比较慢,而且可读性差,所以不到万不得已不要使用list

[1] 译注:这里指T1,T2,T3...重复Tx的模式。

[2] 译注:<T0,T1,none>, <T1,none,none>, <none,none,none> 就是所谓的“机械味”的代码。而template<> struct tiny_size 以及 mpl::int_ 就是其余的代码,在每次的偏特化版本中,这些代码都会被不必要的重复,在大多数情况下,这种代码占多数,且可能具有相当大的数量。

[3] 译注:前面只提到过“模板元程序”,其实用宏也可以编写一个“预处理元程序”,其目标是生成代码,执行者是C/C++预处理器,这就是本章的要点。相应的,还有“预处理元编程”。其原文是“preprocessor metaprogramming”,之所以不译为“预处理器元编程”,是因为后者可能会引起误导——“预处理器”会“元编程”吗?而“预处理元编程”,“预处理元程序”含义很明了——编译预处理期的“程序”或“编程”。

[4] GCC-P选项禁止在预处理结果输出中包含源文件和行号标记。具体请参考GCC手册。

[5] 关于宏展开,我们忽略了许多细节,建议你看一看C++标准的16.3

[6] C99的预处理器可以做到,并且更多。C++标准委员会在C++的下一个标准中倾向于接受C99中的预处理器扩展功能。

[7] 如果出现后缀为_1ST,_2ND,_3RD的宏,它们也应该被忽略,原因是它们将被从库中移掉。

[8] ~”并非完全任意,“@”和“$”本该是不错的选择,只可惜它们并不属于C++实现必须支持的基本字符集。而像“ignored”这样的标识符则可能本身就是宏,会展开,从而导致意想不到的结果。

[9] 注意,“/”及其后面的换行符会被预处理器移掉,所以结果代码实际上只有一行!

[10] 另外的一些预编译器则可以轻松的处理256*256的嵌套重复。

[11] 译注:虽然BOOST_PP_LOCAL_ITERATOR()最终替换为位于BPL中的一个文件名,但是这个文件也将接受预处理,它相当于一个代码模板(此“模板”非C++中的模板),在预处理时会根据用户定义的BOOST_PP_LOCAL_MACRO宏生成相应的代码。其实这种模式与前面的横向重复并无不同,只不过是不断的include文件而已,由于预处理器包含文件的速度较快,所以这是一种“优化”。而后面的“文件迭代”则是对生成的代码的格式的“优化”。

[12] 译注:这一句“严重”意译:-),原文为“although separate instances appear to occupy the same source lines in the debugger, we do have the experience of stepping through the function's source code.”如果直译则难以理解,其实这句话的意思是“跟踪以C++模板或宏组织成的“样板”代码生成的代码时,你只能跟踪到“样板”代码,而无法跟踪到由“样板”代码生成的代码,尽管它们已经被预处理器生成了,而无论你实际上跟踪的是生成的哪一份“特化”的代码,你总是看到“样板”代码中与其对应的行。”

[13] 译注:这里是意译,原文是“The trailing 1 indicates that this is the first nesting level of file iteration should we need to invoke file iteration again from within tiny_size_spec.hpp, we'd need to use BOOST_FILENAME_2 instead.”如果将开始的半句中的“...first nesting level of file iteration”译为“文件迭代的第一重(层)”的话,难免会被误解为“第一次#include该‘样板’文件”。但作者实际要表达的意思却是:每一个“样板”文件对应的是一个level,“样板”文件本身也可以利用其它“样板”文件进行迭代(嵌套),这时候为了将各个“样板”区分开来,只有通过加_N后缀的方式。

[14] 译注:“包含哨卫”用于防止头文件被用户多次#include从而引起重复定义错误,其一般形式为:

#ifndef __INCLUDED_XXX

#define __INCLUDED_XXX

... //codes go here

#endif

这样,一旦头文件被包含入某个文件,__INCLUDED_XXX宏就被定义了,从而如果试图再次包含这个文件,文件中的代码就不再会被处理。这里tiny_size_spec.hpp并不需要“包含哨卫”,因为它并非用于被用户直接包含,而是作为“样板”代码文件被BPL库用来生成一份份的代码实体的,如果使用了“包含哨卫”反而会使迭代的后续步骤都无效。

[15] 虽然继承自std::unary_functionstd::binary_function对于和一些老旧的库之间的交互可能是必须的,但是这样做可能会阻止“空基类优化”(Empty Base Optimization)——考虑当这样两个派生类的对象是同一个对象的一部分(子对象)的情况。更多关于这方面的信息请参见第九章关于结构选择的小节。通常,直接typedef first_argument_typesecond_argument_type以及result_type是比较好的选择。

你可能感兴趣的:(programming)