| 订阅 | 在线投稿
分享
 
 
 

附录 A. 预处理元编程(Preprocessor Metaprogramming).(1)

来源:互联网网民  宽屏版  评论
2006-01-09 06:43:58

http://www.boost-consulting.com/mplbook

David Abraham 著

刘未鹏 译

动机

即便有模板元编程和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 -I. 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宏接受三个参数:

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

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

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

再后面,BOOST_PP_ENUM生成TINY_MAX_SIZE-n个以逗号分隔的“none”。除了每次重复都以逗号分隔之外,BOOST_PP_ENUM和BOOST_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——认识这一点非常重要[10]。通常,当BPL中的某个宏需要接受一个数值参数时,该数值参数必须以单个token的形式被传递。如果在上面的例子中,我们写的是TINY_MAX_SIZE-n,而不是BOOST_PP_SUB(TINY_MAX_SIZE,n),则BOOST_PP_ENUM的第一个参数就包含了三个tokens:3-0(或3-1,或3-2,具体要看是在哪一次重复)。而BOOST_PP_SUB则能够生成单个的token,对于BOOST_PP_SUB(3,0),其生成的是3,BOOST_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_SIZE为3,则问题可能不大,但是在目前仍在被使用的预编译器中,至少有一个在TINY_MAX_SIZE达到8的时候会显著的变慢[11]

而解决这些问题的方案自然是纵向重复。纵向重复可以在不同的行生成具有特定模式的代码。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库中[12]。你会惊讶的发现,与嵌套式的横向重复相比,许多预处理器处理重复的文件包含更快一些。

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

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++模板一样:尽管在调试器中“样板”代码的不同实例看起来重叠在相同的行[13],但是从某种意义上说,我们总算能够单步跟踪函数的源代码了。

要在我们的例子中采取文件迭代,我们可以将前面的局部迭代的代码以及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_LIMITS和BOOST_PP_LOCAL_LIMITS的模式相同,都允许我们提供一个迭代的闭区间。BOOST_PP_FILENAME_1指明了被重复#include的文件名(后面我会为你展示该文件)。后缀_1表示这是进行文件迭代的第一个“样板”文件[14]——如果我们要在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里面并没有使用“包含哨卫[15]”,这是因为该头文件要被多次包含并预处理,每次都会由其中的“样板”代码生成一份不同的代码实体。

现在,预处理结果终于完整保留了“样板”代码的结构了(见下面的结果代码),对于较大的TINY_MAX_SIZE也容易验证了。例如,当TINY_MAX_SIZE为8时,下面的代码摘自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_size和TINY_print在被使用之后立即被#undef了,其间没有任何

被#include的头文件。所以它们可以被看作“局部”的宏定义。因为预处理器无视作

用域的存在,所以为了防止名字冲突,仔细选择名字是非常必要的。我们建议使用

PREFIXED_lower_case这种形式作为局部宏的名字,而PREFIXED_UPPER_CASE

作为全局的。唯一的例外是仅有一个小写字母的名字,你可以用它作为局部宏的名字:

没有任何其它头文件会定义一个全局的单字母小写的宏——那是非常糟糕的风格。


[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]
[11] 另外的一些预编译器则可以轻松的处理256*256的嵌套重复。
[12] 译注:虽然BOOST_PP_LOCAL_ITERATOR()最终替换为位于BPL中的一个文件名,但是这个文件也将接受预处理,它相当于一个代码模板(此“模板”非C++中的模板),在预处理时会根据用户定义的BOOST_PP_LOCAL_MACRO宏生成相应的代码。其实这种模式与前面的横向重复并无不同,只不过是不断的include文件而已,由于预处理器包含文件的速度较快,所以这是一种“优化”。而后面的“文件迭代”则是对生成的代码的格式的“优化”。
[13] 译注:这一句“严重”意译:-),原文为“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++模板或宏组织成的“样板”代码生成的代码时,你只能跟踪到“样板”代码,而无法跟踪到由“样板”代码生成的代码,尽管它们已经被预处理器生成了,而无论你实际上跟踪的是生成的哪一份“特化”的代码,你总是看到“样板”代码中与其对应的行。”
[14] 译注:这里是意译,原文是“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后缀的方式。
[15] 译注:“包含哨卫”用于防止头文件被用户多次#include从而引起重复定义错误,其一般形式为:

#ifndef __INCLUDED_XXX

#define __INCLUDED_XXX

... //codes go here

#endif

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

 
特别声明:以上内容(如有图片或视频亦包括在内)为网络用户发布,本站仅提供信息存储服务。
 
 
[url=http://www.boost-consulting.com/mplbook]http://www.boost-consulting.com/mplbook[/url] David Abraham 著 刘未鹏 译 动机 即便有模板元编程和Boost Metaprogramming Library的强大的能力可供我们支配,一些C++编程任务仍然需要大量地重复样板式的代码。第五章的tiny_size就是个例子: template <class T0, class T1, class T2> struct tiny_size : int_<3> {}; 抛开上面的主模板的模板参数列表中的重复模式[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn1][1][/url]不管,下面还有三个偏特化版本,它们更是遵循一个可预期的模式: 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> {}; 在这种情况下,我们的代码中只有很小一部分具有这种“机械味”,而我们却要为此重复其余的代码[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn2][2][/url],在某些情况下(例如,如果我们实现的是large而非tiny),“其余的”代码量可能相当可观。当一个模式重复出现两三次或更多时,手动书写就容易导致错误。或许更重要的是,代码会变得难以阅读,因为代码中重要的抽象部分其实正是那个模式,而并非遵循该模式的每个代码片断。 代码生成(Code Generation) 抛开手写吧!“机械的”代码应该(也的确可以)被“机械地”生成。对于库的作者,写一个可以生成遵循特定模式的代码片断的程序,然后面临两种选择:一是直接将预生成的源代码文件随库发布,二是发布生成器本身。两者都有缺点。如果客户只得到了预生成的源代码,那么他们就被限制住了——经验表明,这些预生成的代码片断的数目可能今天够用,而明天就不够了!另一方面,如果客户得到了生成程序,那么他们还需要一个可以用来执行该生成程序的程序(例如,解释器),并且,后者必须被整合到build过程中去,除非... 预处理器 ...除非生成程序就是“预处理元程序”[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn3][3][/url]!而“执行(解释)”该元程序的就是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路径里。例如[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn4][4][/url]: g++ -P -E -Ipath/to/boost_1_32_0 -I. test.cpp 有了适当的元程序,我们不但可以调整tiny_size的模板参数的个数,还可以调整tiny的最大尺寸——只要#define TINY_MAX_SIZE为恰当的值即可。 Boost Preprocessor Library[url=http://www.boost.org/libs/preprocessor][MK04][/url]在预处理元编程中充当的角色与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——其中每次出现宏形参的地方都会被替换为用户给出的宏实参[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn5][5][/url]。 宏实参(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 但是,由于逗号的特殊含义,所以在不了解一个宏实参包含多少以逗号分隔的标记序列的情况下[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn6][6][/url],是不可以随便去掉圆括号的。如果你写了一个宏,并要让它能够接受包含任意多个逗号的宏实参(类似于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为后缀[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn7][7][/url]。例如,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并不使用它,所以传递的“~”可以是任意的[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn8][8][/url]。 TINY_size宏每次被BOOST_PP_REPEAT调用时都会生成一个tiny_size的不同的特化版本。TINY_size宏接受三个参数: l z和前面提到的_Z宏相关。它仅被用于优化的目的。目前我们可以忽略它。 l n表示当前为第几次重复。在每次重复调用TINY_size的过程中,n依次为0,1,2...。 l unused,对于这个例子,在每次重复中都是“~”。 通常,BOOST_PP_REPEAT会将用户传给它的参数原封不动的转发给被调用的宏( 例如,TINY_size)。 因为像TINY_size这样的宏的替换文本有好几行,所以除了最后一行,其它行都以反斜杠“\”结尾。其开头的几行调用了BOOST_PP_ENUM来生成以逗号分隔的(模板参数)列表,所以,TINY_size每次被调用都会生成类似下面的代码[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn9][9][/url]: template <class T1, class T2, ... class Tn-1> struct tiny_size< T1, T2, ... Tn-1 ...more... > : mpl::int_<n> {}; BOOST_PP_COMMA_IF宏如果接受的参数非零,就会生成一个逗号,否则为空。当n为0时,BOOST_PP_ENUM_PARAMS(n,T)什么也不会生成,而紧跟在它后面的BOOST_PP_COMMA_IF(n)也为空,因为“<”后面直接跟“,”是非法的。 再后面,BOOST_PP_ENUM生成TINY_MAX_SIZE-n个以逗号分隔的“none”。除了每次重复都以逗号分隔之外,BOOST_PP_ENUM和BOOST_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——认识这一点非常重要[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn10][10][/url]。通常,当BPL中的某个宏需要接受一个数值参数时,该数值参数必须以单个token的形式被传递。如果在上面的例子中,我们写的是TINY_MAX_SIZE-n,而不是BOOST_PP_SUB(TINY_MAX_SIZE,n),则BOOST_PP_ENUM的第一个参数就包含了三个tokens:3-0(或3-1,或3-2,具体要看是在哪一次重复)。而BOOST_PP_SUB则能够生成单个的token,对于BOOST_PP_SUB(3,0),其生成的是3,BOOST_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_SIZE为3,则问题可能不大,但是在目前仍在被使用的预编译器中,至少有一个在TINY_MAX_SIZE达到8的时候会显著的变慢[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn11][11][/url]。 而解决这些问题的方案自然是纵向重复。纵向重复可以在不同的行生成具有特定模式的代码。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库中[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn12][12][/url]。你会惊讶的发现,与嵌套式的横向重复相比,许多预处理器处理重复的文件包含更快一些。 现在,如果我们将新的例子扔给预处理器,就会得到下面的结果: 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++模板一样:尽管在调试器中“样板”代码的不同实例看起来重叠在相同的行[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn13][13][/url],但是从某种意义上说,我们总算能够单步跟踪函数的源代码了。 要在我们的例子中采取文件迭代,我们可以将前面的局部迭代的代码以及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_LIMITS和BOOST_PP_LOCAL_LIMITS的模式相同,都允许我们提供一个迭代的闭区间。BOOST_PP_FILENAME_1指明了被重复#include的文件名(后面我会为你展示该文件)。后缀_1表示这是进行文件迭代的第一个“样板”文件[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn14][14][/url]——如果我们要在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里面并没有使用“包含哨卫[url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftn15][15][/url]”,这是因为该头文件要被多次包含并预处理,每次都会由其中的“样板”代码生成一份不同的代码实体。 现在,预处理结果终于完整保留了“样板”代码的结构了(见下面的结果代码),对于较大的TINY_MAX_SIZE也容易验证了。例如,当TINY_MAX_SIZE为8时,下面的代码摘自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_size和TINY_print在被使用之后立即被#undef了,其间没有任何 被#include的头文件。所以它们可以被看作“局部”的宏定义。因为预处理器无视作 用域的存在,所以为了防止名字冲突,仔细选择名字是非常必要的。我们建议使用 PREFIXED_lower_case这种形式作为局部宏的名字,而PREFIXED_UPPER_CASE 作为全局的。唯一的例外是仅有一个小写字母的名字,你可以用它作为局部宏的名字: 没有任何其它头文件会定义一个全局的单字母小写的宏——那是非常糟糕的风格。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref1][1][/url] 译注:这里指T1,T2,T3...重复Tx的模式。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref2][2][/url] 译注:<T0,T1,none>, <T1,none,none>, <none,none,none> 就是所谓的“机械味”的代码。而template<> struct tiny_size 以及 mpl::int_ 就是其余的代码,在每次的偏特化版本中,这些代码都会被不必要的重复,在大多数情况下,这种代码占多数,且可能具有相当大的数量。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref3][3][/url] 译注:前面只提到过“模板元程序”,其实用宏也可以编写一个“预处理元程序”,其目标是生成代码,执行者是C/C++预处理器,这就是本章的要点。相应的,还有“预处理元编程”。其原文是“preprocessor metaprogramming”,之所以不译为“预处理器元编程”,是因为后者可能会引起误导——“预处理器”会“元编程”吗?而“预处理元编程”,“预处理元程序”含义很明了——编译预处理期的“程序”或“编程”。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref4][4][/url] GCC的-P选项禁止在预处理结果输出中包含源文件和行号标记。具体请参考GCC手册。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref5][5][/url] 关于宏展开,我们忽略了许多细节,建议你看一看C++标准的16.3。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref6][6][/url] C99的预处理器可以做到,并且更多。C++标准委员会在C++的下一个标准中倾向于接受C99中的预处理器扩展功能。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref7][7][/url] 如果出现后缀为_1ST,_2ND,或_3RD的宏,它们也应该被忽略,原因是它们将被从库中移掉。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref8][8][/url] “~”并非完全任意,“@”和“$”本该是不错的选择,只可惜它们并不属于C++实现必须支持的基本字符集。而像“ignored”这样的标识符则可能本身就是宏,会展开,从而导致意想不到的结果。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref9][9][/url] 注意,“\”及其后面的换行符会被预处理器移掉,所以结果代码实际上只有一行! [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref10][10][/url] [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref11][11][/url] 另外的一些预编译器则可以轻松的处理256*256的嵌套重复。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref12][12][/url] 译注:虽然BOOST_PP_LOCAL_ITERATOR()最终替换为位于BPL中的一个文件名,但是这个文件也将接受预处理,它相当于一个代码模板(此“模板”非C++中的模板),在预处理时会根据用户定义的BOOST_PP_LOCAL_MACRO宏生成相应的代码。其实这种模式与前面的横向重复并无不同,只不过是不断的include文件而已,由于预处理器包含文件的速度较快,所以这是一种“优化”。而后面的“文件迭代”则是对生成的代码的格式的“优化”。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref13][13][/url] 译注:这一句“严重”意译:-),原文为“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++模板或宏组织成的“样板”代码生成的代码时,你只能跟踪到“样板”代码,而无法跟踪到由“样板”代码生成的代码,尽管它们已经被预处理器生成了,而无论你实际上跟踪的是生成的哪一份“特化”的代码,你总是看到“样板”代码中与其对应的行。” [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref14][14][/url] 译注:这里是意译,原文是“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后缀的方式。 [url=file:///G:/我的文档/我的翻译/Preprocessor.htm#_ftnref15][15][/url] 译注:“包含哨卫”用于防止头文件被用户多次#include从而引起重复定义错误,其一般形式为: #ifndef __INCLUDED_XXX #define __INCLUDED_XXX ... //codes go here #endif 这样,一旦头文件被包含入某个文件,__INCLUDED_XXX宏就被定义了,从而如果试图再次包含这个文件,文件中的代码就不再会被处理。这里tiny_size_spec.hpp并不需要“包含哨卫”,因为它并非用于被用户直接包含,而是作为“样板”代码文件被BPL库用来生成一份份的代码实体的,如果使用了“包含哨卫”反而会使迭代的后续步骤都无效。
󰈣󰈤
 
 
 
>>返回首页<<
 
 热帖排行
 
 
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
©2005- 王朝网络 版权所有