`
61party
  • 浏览: 1055520 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

《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>

<p cla
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics